As we saw in §15.2.2 (p. 598), the initialization phase of a derived-class constructor initializes the base-class part(s) of a derived object as well as initializing its own members. As a result, the copy and move constructors for a derived class must copy/move the members of its base part as well as the members in the derived. Similarly, a derived-class assignment operator must assign the members in the base part of the derived object.
Unlike the constructors and assignment operators, the destructor is responsible only for destroying the resources allocated by the derived class. Recall that the members of an object are implicitly destroyed (§13.1.3, p. 502). Similarly, the base-class part of a derived object is destroyed automatically.
When a derived class defines a copy or move operation, that operation is responsible for copying or moving the entire object, including base-class members.
When we define a copy or move constructor (§13.1.1, p. 496, and §13.6.2, p. 534) for a derived class, we ordinarily use the corresponding base-class constructor to initialize the base part of the object:
class Base { /* ... */ } ;
class D: public Base {
public:
// by default, the base class default constructor initializes the base part of an object
// to use the copy or move constructor, we must explicitly call that
// constructor in the constructor initializer list
D(const D& d): Base(d) // copy the base members
/* initializers for members of D */ { /* ... */ }
D(D&& d): Base(std::move(d)) // move the base members
/* initializers for members of D */ { /* ... */ }
};
The initializer Base(d)
passes a D
object to a base-class constructor. Although in principle, Base
could have a constructor that has a parameter of type D
, in practice, that is very unlikely. Instead, Base(d)
will (ordinarily) match the Base
copy constructor. The D
object, d
, will be bound to the Base&
parameter in that constructor. The Base
copy constructor will copy the base part of d
into the object that is being created. Had the initializer for the base class been omitted,
// probably incorrect definition of the D copy constructor
// base-class part is default initialized, not copied
D(const D& d) /* member initializers, but no base-class initializer */
{ /* ... */ }
the Base
default constructor would be used to initialize the base part of a D
object. Assuming D
’s constructor copies the derived members from d
, this newly constructed object would be oddly configured: Its Base
members would hold default values, while its D
members would be copies of the data from another object.
By default, the base-class default constructor initializes the base-class part of a derived object. If we want copy (or move) the base-class part, we must explicitly use the copy (or move) constructor for the base class in the derived’s constructor initializer list.
Like the copy and move constructors, a derived-class assignment operator (§13.1.2, p. 500, and §13.6.2, p. 536), must assign its base part explicitly:
// Base::operator=(const Base&) is not invoked automatically
D &D::operator=(const D &rhs)
{
Base::operator=(rhs); // assigns the base part
// assign the members in the derived class, as usual,
// handling self-assignment and freeing existing resources as appropriate
return *this;
}
This operator starts by explicitly calling the base-class assignment operator to assign the members of the base part of the derived object. The base-class operator will (presumably) correctly handle self-assignment and, if appropriate, will free the old value in the base part of the left-hand operand and assign the new values from rhs
. Once that operator finishes, we continue doing whatever is needed to assign the members in the derived class.
It is worth noting that a derived constructor or assignment operator can use its corresponding base class operation regardless of whether the base defined its own version of that operator or uses the synthesized version. For example, the call to Base::operator=
executes the copy-assignment operator in class Base
. It is immaterial whether that operator is defined explicitly by the Base
class or is synthesized by the compiler.
Recall that the data members of an object are implicitly destroyed after the destructor body completes (§13.1.3, p. 502). Similarly, the base-class parts of an object are also implicitly destroyed. As a result, unlike the constructors and assignment operators, a derived destructor is responsible only for destroying the resources allocated by the derived class:
class D: public Base {
public:
// Base::~Base invoked automatically
~D() { /* do what it takes to clean up derived members */ }
};
Objects are destroyed in the opposite order from which they are constructed: The derived destructor is run first, and then the base-class destructors are invoked, back up through the inheritance hierarchy.
As we’ve seen, the base-class part of a derived object is constructed first. While the base-class constructor is executing, the derived part of the object is uninitialized. Similarly, derived objects are destroyed in reverse order, so that when a base class destructor runs, the derived part has already been destroyed. As a result, while these base-class members are executing, the object is incomplete.
To accommodate this incompleteness, the compiler treats the object as if its type changes during construction or destruction. That is, while an object is being constructed it is treated as if it has the same class as the constructor; calls to virtual functions will be bound as if the object has the same type as the constructor itself. Similarly, for destructors. This binding applies to virtuals called directly or that are called indirectly from a function that the constructor (or destructor) calls.
To understand this behavior, consider what would happen if the derived-class version of a virtual was called from a base-class constructor. This virtual probably accesses members of the derived object. After all, if the virtual didn’t need to use members of the derived object, the derived class probably could use the version in its base class. However, those members are uninitialized while a base constructor is running. If such access were allowed, the program would probably crash.
If a constructor or destructor calls a virtual, the version that is run is the one corresponding to the type of the constructor or destructor itself.
Exercise 15.26: Define the Quote
and Bulk_quote
copy-control members to do the same job as the synthesized versions. Give them and the other constructors print statements that identify which function is running. Write programs using these classes and predict what objects will be created and destroyed. Compare your predictions with the output and continue experimenting until your predictions are reliably correct.