15.7. Constructors and Copy Control
Like any other class, a class in an inheritance hierarchy controls what happens when objects of its type are created, copied, moved, assigned, or destroyed. As for any other class, if a class (base or derived) does not itself define one of the copy-control operations, the compiler will synthesize that operation. Also, as usual, the synthesized version of any of these members might be a deleted function.
15.7.1. Virtual Destructors
FundamentalThe primary direct impact that inheritance has on copy control for a base class is that a base class generally should define a virtual destructor (§15.2.1, p. 594). The destructor needs to be virtual to allow objects in the inheritance hierarchy to be dynamically allocated.
Recall that the destructor is run when we delete
a pointer to a dynamically allocated object (§13.1.3, p. 502). If that pointer points to a type in an inheritance hierarchy, it is possible that the static type of the pointer might differ from the dynamic type of the object being destroyed (§15.2.2, p. 597). For example, if we delete
a pointer of type Quote*
, that pointer might point at a Bulk_quote
object. If the pointer points at a Bulk_quote
, the compiler has to know that it should run the Bulk_quote
destructor. As with any other function, we arrange to run the proper destructor by defining the destructor as virtual in the base class:
class Quote {
public:
// virtual destructor needed if a base pointer pointing to a derived object is deleted
virtual ~Quote() = default; // dynamic binding for the destructor
};
Like any other virtual, the virtual nature of the destructor is inherited. Thus, classes derived from Quote
have virtual destructors, whether they use the synthesized destructor or define their own version. So long as the base class destructor is virtual, when we delete
a pointer to base, the correct destructor will be run:
Quote *itemP = new Quote; // same static and dynamic type
delete itemP; // destructor for Quote called
itemP = new Bulk_quote; // static and dynamic types differ
delete itemP; // destructor for Bulk_quote called
WARNING
Executing delete
on a pointer to base that points to a derived object has undefined behavior if the base’s destructor is not virtual.
Destructors for base classes are an important exception to the rule of thumb that if a class needs a destructor, it also needs copy and assignment (§13.1.4, p. 504). A base class almost always needs a destructor, so that it can make the destructor virtual. If a base class has an empty destructor in order to make it virtual, then the fact that the class has a destructor does not indicate that the assignment operator or copy constructor is also needed.
Virtual Destructors Turn Off Synthesized Move
The fact that a base class needs a virtual destructor has an important indirect impact on the definition of base and derived classes: If a class defines a destructor—even if it uses = default
to use the synthesized version—the compiler will not synthesize a move operation for that class (§13.6.2, p. 537).
INFO
Exercises Section 15.7.1
Exercise 15.24: What kinds of classes need a virtual destructor? What operations must a virtual destructor perform?
15.7.2. Synthesized Copy Control and Inheritance
FundamentalThe synthesized copy-control members in a base or a derived class execute like any other synthesized constructor, assignment operator, or destructor: They memberwise initialize, assign, or destroy the members of the class itself. In addition, these synthesized members initialize, assign, or destroy the direct base part of an object by using the corresponding operation from the base class. For example,
- The synthesized
Bulk_quote
default constructor runs theDisc_Quote
default constructor, which in turn runs theQuote
default constructor. - The
Quote
default constructor default initializes thebookNo
member to the empty string and uses the in-class initializer to initializeprice
to zero. - When the
Quote
constructor finishes, theDisc_Quote
constructor continues, which uses the in-class initializers to initializeqty
anddiscount
. - When the
Disc_quote
constructor finishes, theBulk_quote
constructor continues but has no other work to do.
Similarly, the synthesized Bulk_quote
copy constructor uses the (synthesized) Disc_quote
copy constructor, which uses the (synthesized) Quote
copy constructor. The Quote
copy constructor copies the bookNo
and price
members; and the Disc_Quote
copy constructor copies the qty
and discount
members.
It is worth noting that it doesn’t matter whether the base-class member is itself synthesized (as is the case in our Quote
hierarchy) or has a an user-provided definition. All that matters is that the corresponding member is accessible (§15.5, p. 611) and that it is not a deleted function.
Each of our Quote
classes use the synthesized destructor. The derived classes do so implicitly, whereas the Quote
class does so explicitly by defining its (virtual) destructor as = default
. The synthesized destructor is (as usual) empty and its implicit destruction part destroys the members of the class (§13.1.3, p. 501). In addition to destroying its own members, the destruction phase of a destructor in a derived class also destroys its direct base. That destructor in turn invokes the destructor for its own direct base, if any. And, so on up to the root of the hierarchy.
As we’ve seen, Quote
does not have synthesized move operations because it defines a destructor. The (synthesized) copy operations will be used whenever we move a Quote
object (§13.6.2, p. 540). As we’re about to see, the fact that Quote
does not have move operations means that its derived classes don’t either.
Base Classes and Deleted Copy Control in the Derived
C++11The synthesized default constructor, or any of the copy-control members of either a base or a derived class, may be defined as deleted for the same reasons as in any other class (§13.1.6, p. 508, and §13.6.2, p. 537). In addition, the way in which a base class is defined can cause a derived-class member to be defined as deleted:
- If the default constructor, copy constructor, copy-assignment operator, or destructor in the base class is deleted or inaccessible (§15.5, p. 612), then the corresponding member in the derived class is defined as deleted, because the compiler can’t use the base-class member to construct, assign, or destroy the base-class part of the object.
- If the base class has an inaccessible or deleted destructor, then the synthesized default and copy constructors in the derived classes are defined as deleted, because there is no way to destroy the base part of the derived object.
- As usual, the compiler will not synthesize a deleted move operation. If we use
= default
to request a move operation, it will be a deleted function in the derived if the corresponding operation in the base is deleted or inaccessible, because the base class part cannot be moved. The move constructor will also be deleted if the base class destructor is deleted or inaccessible.
As an example, this base class, B
,
class B {
public:
B();
B(const B&) = delete;
// other members, not including a move constructor
};
class D : public B {
// no constructors
};
D d; // ok: D's synthesized default constructor uses B's default constructor
D d2(d); // error: D's synthesized copy constructor is deleted
D d3(std::move(d)); // error: implicitly uses D's deleted copy constructor
has an accessible default constructor and an explicitly deleted copy constructor. Because the copy constructor is defined, the compiler will not synthesize a move constructor for class B
(§13.6.2, p. 537). As a result, we can neither move nor copy objects of type B
. If a class derived from B
wanted to allow its objects to be copied or moved, that derived class would have to define its own versions of these constructors. Of course, that class would have to decide how to copy or move the members in it base-class part. In practice, if a base class does not have a default, copy, or move constructor, then its derived classes usually don’t either.
Move Operations and Inheritance
As we’ve seen, most base classes define a virtual destructor. As a result, by default, base classes generally do not get synthesized move operations. Moreover, by default, classes derived from a base class that doesn’t have move operations don’t get synthesized move operations either.
Because lack of a move operation in a base class suppresses synthesized move for its derived classes, base classes ordinarily should define the move operations if it is sensible to do so. Our Quote
class can use the synthesized versions. However, Quote
must define these members explicitly. Once it defines its move operations, it must also explicitly define the copy versions as well (§13.6.2, p. 539):
class Quote {
public:
Quote() = default; // memberwise default initialize
Quote(const Quote&) = default; // memberwise copy
Quote(Quote&&) = default; // memberwise copy
Quote& operator=(const Quote&) = default; // copy assign
Quote& operator=(Quote&&) = default; // move assign
virtual ~Quote() = default;
// other members as before
};
Now, Quote
objects will be memberwise copied, moved, assigned, and destroyed. Moreover, classes derived from Quote
will automatically obtain synthesized move operations as well, unless they have members that otherwise preclude move.
INFO
Exercises Section 15.7.2
Exercise 15.25: Why did we define a default constructor for Disc_quote
? What effect, if any, would removing that constructor have on the behavior of Bulk_quote
?
15.7.3. Derived-Class Copy-Control Members
FundamentalAs 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.
WARNING
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.
Defining a Derived Copy or Move Constructor
TrickyWhen 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.
WARNING
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.
Derived-Class Assignment Operator
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.
Derived-Class Destructor
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.
Calls to Virtuals in Constructors and Destructors
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.
INFO
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.
INFO
Exercises Section 15.7.3
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.
15.7.4. Inherited Constructors
C++11Under the new standard, a derived class can reuse the constructors defined by its direct base class. Although, as we’ll see, such constructors are not inherited in the normal sense of that term, it is nonetheless common to refer to such constructors as “inherited.” For the same reasons that a class may initialize only its direct base class, a class may inherit constructors only from its direct base. A class cannot inherit the default, copy, and move constructors. If the derived class does not directly define these constructors, the compiler synthesizes them as usual.
A derived class inherits its base-class constructors by providing a using
declaration that names its (direct) base class. As an example, we can redefine our Bulk_quote
class (§15.4, p. 610) to inherit its constructors from Disc_quote
:
class Bulk_quote : public Disc_quote {
public:
using Disc_quote::Disc_quote; // inherit Disc_quote's constructors
double net_price(std::size_t) const;
};
Ordinarily, a using
declaration only makes a name visible in the current scope. When applied to a constructor, a using
declaration causes the compiler to generate code. The compiler generates a derived constructor corresponding to each constructor in the base. That is, for each constructor in the base class, the compiler generates a constructor in the derived class that has the same parameter list.
These compiler-generated constructors have the form
derived(parms) : base(args) { }
where derived is the name of the derived class, base is the name of the base class, parms is the parameter list of the constructor, and args pass the parameters from the derived constructor to the base constructor. In our Bulk_quote
class, the inherited constructor would be equivalent to
Bulk_quote(const std::string& book, double price,
std::size_t qty, double disc):
Disc_quote(book, price, qty, disc) { }
If the derived class has any data members of its own, those members are default initialized (§7.1.4, p. 266).
Characteristics of an Inherited Constructor
Unlike using
declarations for ordinary members, a constructor using
declaration does not change the access level of the inherited constructor(s). For example, regardless of where the using
declaration appears, a private
constructor in the base is a private
constructor in the derived; similarly for protected
and public
constructors.
Moreover, a using
declaration can’t specify explicit
or constexpr
. If a constructor in the base is explicit
(§7.5.4, p. 296) or constexpr
(§7.5.6, p. 299), the inherited constructor has the same property.
If a base-class constructor has default arguments (§6.5.1, p. 236), those arguments are not inherited. Instead, the derived class gets multiple inherited constructors in which each parameter with a default argument is successively omitted. For example, if the base has a constructor with two parameters, the second of which has a default, the derived class will obtain two constructors: one with both parameters (and no default argument) and a second constructor with a single parameter corresponding to the left-most, non-defaulted parameter in the base class.
If a base class has several constructors, then with two exceptions, the derived class inherits each of the constructors from its base class. The first exception is that a derived class can inherit some constructors and define its own versions of other constructors. If the derived class defines a constructor with the same parameters as a constructor in the base, then that constructor is not inherited. The one defined in the derived class is used in place of the inherited constructor.
The second exception is that the default, copy, and move constructors are not inherited. These constructors are synthesized using the normal rules. An inherited constructor is not treated as a user-defined constructor. Therefore, a class that contains only inherited constructors will have a synthesized default constructor.
INFO
Exercises Section 15.7.4
Exercise 15.27: Redefine your Bulk_quote
class to inherit its constructors.