Why is this an issue?
When constructing an object of a derived class, the sub-object of the base class is constructed first, and only then the constructor of the derived
class is called. When there are multiple levels of inheritance, the process is the same, from the most base class to the most derived class. Along
this construction process, the dynamic type of the object evolves and is the type of the sub-object under construction.
The destruction of the object follows the same process in reverse order.
As a consequence, when calling a virtual function from a constructor or a destructor, the actual function being called is not necessarily the
version from the most-derived type, as some developers may believe, but the version that matches the level under construction.
struct A {
virtual void f();
virtual void g();
virtual void h() = 0;
};
struct B : public A {
B() {
f();
g();
}
void f() override;
};
struct C : public B {
void f() override;
void g() override;
void h() override;
};
When constructing an object of type C, the following occurs:
- The sub-object of type
A
is constructed.
- The sub-object of type
B
is constructed.
- The constructor
B::B()
is called, during this call, *this
is considered as being of type B
.
- The function
B::f()
is called.
- The function
A::g()
is called.
- The object of type
C
is constructed.
This surprising behavior can be even worse: If there is no implementation for a virtual function (in the example, if the constructor attempted to
call h()
which is still a pure virtual function) the behavior is undefined.
If you want to perform virtual calls during object construction that will consider the actual type of the object, the best way is probably to defer
those calls right after the object is constructed, by using a factory function:
std::unique_ptr<Base> createObjectOfDerivedType(parameters) {
auto result = ...;
result->callVirtualFunction();
return result;
}
This rule raises an issue when a non-final virtual function is called from a constructor or a destructor, therefore avoiding all surprising
behavior.
Noncompliant code example
class Parent {
public:
Parent() {
f1();
f2(); // Noncompliant; confusing because Parent::f2() will always be called even if it is overridden
}
virtual ~Parent() {
f3(); // Noncompliant; undefined behavior
}
private:
int f1();
virtual void f2();
virtual void f3() = 0; // pure virtual
};
class Child : public Parent {
public:
Child() { // leads to a call to Parent::f2(), not Child::f2()
f3(); // Noncompliant; Child::f3() might be further overridden
}
protected:
void f2() override;
void f3() override;
};
Compliant solution
class Parent {
public:
Parent() {
f1();
Parent::f2(); // acceptable but poor design
}
virtual ~Parent() {
// call to pure virtual function removed
}
protected:
void f1();
virtual void f2();
virtual void f3() = 0;
};
class Child : public Parent {
public:
Child() {
}
virtual ~Child() {
f3(); // // Compliant - Well defined and predictable, a final function cannot be further overridden
}
protected:
void f2() override;
void f3() final;
};
Resources