When constructing an object of a derived class, the sub-object of the base class is constructed first, and only then is the derived class’s
constructor called. This process remains the same when there are multiple levels of inheritance, from the most base class to the most derived class.
During construction, the object’s dynamic type evolves to become the type of the sub-object under construction. The destruction of the object follows
the same process in reverse order.
These rules for C++ mean that invoking a virtual function from a constructor (or a destructor) selects the override that matches the level under
construction (or destruction). This is not necessarily the override from the most derived type, contrary to what developers familiar with other
programming languages might expect.
Additionally, the behavior is undefined when the selected override is a pure virtual function.
We illustrate C++'s behavior in the following example.
struct Base {
virtual std::string getPrefix() { return "Default"; }
virtual std::string getClassName() { return "Base"; }
};
struct Derived : Base {
std::string getClassName() override { return "Derived"; }
Derived() {
std::cout << getPrefix() << " - " << getClassName() << '\n';
}
};
struct Subderived : Derived {
std::string getPrefix() override { return "Custom"; }
std::string getClassName() final { return "Subderived"; }
};
Constructing an object of type Subderived
prints Default - Derived. In detail, the following occurs:
- The sub-object of type
Base
is constructed using the compiler-generated constructor.
- The sub-object of type
Derived
is constructed using the user-provided constructor.
- This constructor considers
*this
as being of type Derived
.
- The function
Base::getPrefix()
is called.
- The function
Derived::getClassName()
is called.
- Finally, the object of type
Subderived
is constructed.
The fact that Subderived
's methods are declared with the override
and final
keywords does not play any
role.
This rule raises an issue when a non-final virtual function is called from a constructor or a destructor.
What is the potential impact?
In the best-case scenario, the selected override is the desired one. However, this may not be obvious to everyone reading the code and can cause
needless confusion. Secondly, this reduces the software’s adaptability: changing the class hierarchy may break the current assumption and lead to bugs
if the wrong override is selected.
Another likely scenario is that the wrong overload is selected. Since the difference between the program’s expected and actual behavior can be very
small, you may spend a significant amount of time identifying and fixing the problem. The problem is even more challenging to identify when virtual
functions are called indirectly through another function.
Finally, since the behavior is undefined if the chosen override is a pure virtual function, the program might crash or produce obvious incorrect
results. Or it might seem to work fine on the surface, yet lead to bigger problems down the line that are not identified by tests.