The vulnerability as described in ISO/IEC 24772-1:2024 6.41 is applicable to C++.
In C++, inheritance is defined differently than in most other languages that support inheritance, e.g.: - There is no implicit common base class. - Defining a member with a name that is defined in a base class causes hiding the base class member in the derived class including all overloads with that name. (Full qualification via the base class name is required to access these hidden members.) - Dynamic polymorphism requires the use of references or pointers. - Private virtual member functions can be overridden in derived classes.
A vulnerability similar to namespace issues (see [BJL]) can arise if a new final overrider is defined, which can result in a silent change to the semantics of an existing program.
The implementation of virtual member functions does not take access into account. This can lead to a potentially surprising situation where a virtual function overrides an otherwise inaccessible base class function. This is quite different than the situation for non-virtual private member functions, which cannot be accessed from outside the declaring class or any friend
s.
Inheriting from a non-polymorphic base class can be used to implement adaptor types, i.e., by limiting or extending the base’s functionality. Using private inheritance can prevent treating the derived type object as a base type object. Inheriting constructors and using declarations ease re-promoting the scope of otherwise hidden members of the base.
Multiple inheritance adds an additional dimension since a class can inherit the same base class A
indirectly via different direct base classes. Without any special preparation, this means the base class A
object exists multiple times. Addressing members of A
explicitly requires to specify the differentiating base class as a prefix, otherwise the code will be ambiguous. If all classes in such a multiple inheritance hierarchy that directly inherit from A
use the keyword virtual
when inheriting from A
, there will be only one object of type A
in the most derived object. Inconsistently inheriting from A
with and without virtual
might lead to confusing behavior, because still multiple base objects of type A
exist. Even when an ambiguity does not currently exist, refering to a base class member using its qualified name reduces the chance that a future modification silently changes the program’s behaviour through accessing a different member with the same name.
In C++, a name declared in a virtual base and inherited multiple times but hidden on any virtual path is considered to be hidden on all virtual paths. Consequently, a hiding declaration introduced later will silently alter the semantics of code refering to this name. The vulnerability does not arise if a multiply-inherited name is found on at least one non-virtual inheritance path, since the use of the name is diagnosed as ambiguous.
Inheriting from base classes without virtual member functions and without non-static data members (empty bases) does not suffer from the multiple object problem of multiple inheritance, because the empty base class object will be omitted (elided) by the compiler (empty base class optimization). Such empty bases are often used to mix-in functionality into derived classes.
The compiler-provided default behaviour for copy and move operations as well as destruction favors value semantics which conflicts with object-oriented polymorphic behaviour.
Virtual Destructor: This means, base classes that define virtual member functions will need to also define a virtual destructor and in addition need to care about the copy and move operations, otherwise deleting a dynamically-allocated derived object via a base class pointer will cause undefined behaviour [EWF].
Slicing: A common failure is to not eliminate implicitly callable copy and move operations in base classes which will lead to accidental copying of a base class suboject via a base class reference that actually refers to a derived object. Preventing implicit copy and move operations in base classes defining virtual member functions is a common mitigation.
Incomplete Copy: When a derived class defines its own non-defaulted, non-deleted copy or move operations, care must be taken to actually copy and move all base class subobjects as well. Omitting a base class when defining copy or move constructors means the default construction of a base class object happens. Not invoking a base class assignment in the definition of copy and move assignment operators will cause the base class retaining its previous members and not obtaining the source object’s base members. None of these omissions are a compile error and none are an issue for empty bases.
If a base class overloads operator new
and operator delete
, any derived classes will inherit and therefore will use such. If the base class’ operator new
and/or operator delete
assume the size of the objects being allocated are all the size of the base class and they are not all the same size, then this will result in undefined behaviour such as access errors to memory that wasn’t allocated, overwriting of memory (if there are regions of memory immediately after the last byte allocated), memory leaks, etc.
The mechanisms of failure from ISO/IEC 24772-1:2024 clause 6.41 can be mitigated in C++ as follows:
Execution of malicious redefinitions can be prevented by use of final
on each member function to generate compiler diagnostics when overriding is not permitted.
Accidental redefinition can be mitigated by a project mandate to use the override
or final
special identifiers when overriding a virtual member functions.
Accidental failure of redefinition can be prevented by using override
on each member function intended to be redefined to generate compiler diagnostics when overriding does not apply.
Breaking of class invariants can be avoided by proper initialization even with the default constructor and by defining data members private to ensure that the functions with the appropriate functionality are called and thus class invariants are preserved. If copy and move operations are user-defined in a derived class they must ensure to call the corresponding base class operations.
To avoid the vulnerability or mitigate its ill effects, C++ software developers can:
override
whenever overriding a virtual member function to generate compiler diagnostics for failures to override.final
to prevent an unexpected overriding of that function deeper in the hierarchy.final
or override
when overriding a member function.protected
or private
whenever appropriate.