Section 15.4. Constructors and Copy Control


15.4. Constructors and Copy Control

The fact that each derived object consists of the (nonstatic) members defined in the derived class plus one or more base-class subobjects affects how derived-type objects are constructed, copied, assigned, and destroyed. When we construct, copy, assign, or destroy an object of derived type, we also construct, copy, assign, or destroy those base-class subobjects.

Constructors and the copy-control members are not inherited; each class defines its own constructor(s) and copy-control members. As is the case for any class, synthesized versions of the default constructor and the copy-control members will be used if the class does not define its own versions.

15.4.1. Base-Class Constructors and Copy Control

Constructors and copy control for base classes that are not themselves a derived class are largely unaffected by inheritance. Our Item_base constructor looks like many we've seen before:

      Item_base(const std::string &book = "",                double sales_price = 0.0):                       isbn(book), price(sales_price) { } 

The only impact inheritance has on base-class constructors is that there is a new kind of user that must be considered when deciding which constructors to offer. Like any other member, constructors can be made protected or private. Some classes need special constructors that are intended only for their derived classes to use. Such constructors should be made protected.

15.4.2. Derived-Class Constructors

Derived constructors are affected by the fact that they inherit from another class. Each derived constructor initializes its base class in addition to initializing its own data members.

The Synthesized Derived-Class Default Constructor

A derived-class synthesized default constructor (Section 12.4.3, p. 458) differs from a nonderived constructor in only one way: In addition to initializing the data members of the derived class, it also initializes the base part of its object. The base part is initialized by the default constructor of the base class.

For our Bulk_item class, the synthesized default constructor would execute as follows:

  1. Invoke the Item_base default constructor, which initializes the isbn member to the empty string and the price member to zero.

  2. Initialize the members of Bulk_item using the normal variable initialization rules, which means that the qty and discount members would be uninitialized.

Defining a Default Constructor

Because Bulk_item has members of built-in type, we should define our own default constructor:

      class Bulk_item : public Item_base {      public:          Bulk_item(): min_qty(0), discount(0.0) { }          // as before      }; 

This constructor uses the constructor initializer list (Section 7.7.3, p. 263) to initialize its min_qty and discount members. The constructor initializer also implicitly invokes the Item_base default constructor to initialize its base-class part.

The effect of running this constructor is that first the Item_base part is initialized using the Item_base default constructor. That constructor sets isbn to the empty string and price to zero. After the Item_base constructor finishes, the members of the Bulk_item part are initialized, and the (empty) body of the constructor is executed.

Passing Arguments to a Base-Class Constructor

In addition to the default constructor, our Item_base class lets users initialize the isbn and price members. We'd like to support the same initialization for Bulk_item objects. In fact, we'd like our users to be able to specify values for the entire Bulk_item, including the discount rate and quantity.

The constructor initializer list for a derived-class constructor may initialize only the members of the derived class; it may not directly initialize its inherited members. Instead, a derived constructor indirectly initializes the members it inherits by including its base class in its constructor initializer list:

      class Bulk_item : public Item_base {      public:          Bulk_item(const std::string& book, double sales_price,                    std::size_t qty = 0, double disc_rate = 0.0):                       Item_base(book, sales_price),                       min_qty(qty), discount(disc_rate) { }          // as before       }; 

This constructor uses the two-parameter Item_base constructor to initialize its base subobject. It passes its own book and sales_price arguments to that constructor. We might use this constructor as follows:

      // arguments are the isbn, price, minimum quantity, and discount      Bulk_item bulk("0-201-82470-1", 50, 5, .19); 

bulk is built by first running the Item_base constructor, which initializes isbn and price from the arguments passed in the Bulk_item constructor initializer. After the Item_base constructor finishes, the members of Bulk_item are initialized. Finally, the (empty) body of the Bulk_item constructor is run.

The constructor initializer list supplies initial values for a class' base class and members. It does not specify the order in which those initializations are done. The base class is initialized first and then the members of the derived class are initialized in the order in which they are declared.



Using Default Arguments in a Derived Constructor

Of course, we might write these two Bulk_item constructors as a single constructor that takes default arguments:

      class Bulk_item : public Item_base {      public:          Bulk_item(const std::string& book, double sales_price,                    std::size_t qty = 0, double disc_rate = 0.0):                       Item_base(book, sales_price),                       min_qty(qty), discount(disc_rate) { }          // as before       }; 

Here we provide defaults for each parameter so that the constructor might be used with zero to four arguments.

Only an Immediate Base Class May Be Initialized

A class may initialize only its own immediate base class. An immediate base class is the class named in the derivation list. If class C is derived from class B, which is derived from class A, then B is the immediate base of C. Even though every C object contains an A part, the constructors for C may not initialize the A part directly. Instead, class C initializes B, and the constructor for class B in turn initializes A. The reason for this restriction is that the author of class B has specified how to construct and initialize objects of type B. As with any user of class B, the author of class C has no right to change that specification.

As a more concrete example, our bookstore might have several discount strategies. In addition to a bulk discount, it might offer a discount for purchases up to a certain quantity and then charge the full price thereafter. Or it might offer a discount for purchases above a certain limit but not for purchases up to that limit.

Each of these discount strategies is the same in that it requires a quantity and a discount amount. We might support these differing strategies by defining a new class named Disc_item to store the quantity and the discount amount. This class would not define a net_price function but would serve as a base class for classes such as Bulk_item that define the different discount strategies.

Key Concept: Refactoring

Adding Disc_item to the Item_base hierarchy is an example of refactoring. Refactoring involves redesigning a class hierarchy to move operations and/or data from one class to another. Refactoring happens most often when classes are redesigned to add new functionality or handle other changes in that application's requirements.

Refactoring is common in OO applications. It is noteworthy that even though we changed the inheritance hierarchy, code that uses the Bulk_item or Item_base classes would not need to change. However, when classes are refactored, or changed in any other way, any code that uses those classes must be recompiled.


To implement this design, we first need to define the Disc_item class:

      // class to hold discount rate and quantity      // derived classes will implement pricing strategies using these data      class Disc_item : public Item_base {      public:          Disc_item(const std::string& book = "",                    double sales_price = 0.0,                    std::size_t qty = 0, double disc_rate = 0.0):                       Item_base(book, sales_price),                       quantity(qty), discount(disc_rate) { }          protected:              std::size_t quantity; // purchase size for discount to apply              double discount;      // fractional discount to apply       }; 

This class inherits from Item_base and defines its own members, discount and quantity. Its only member function is the constructor, which initializes its Item_base base class and the members defined by Disc_item.

Next, we can reimplement Bulk_item to inherit from Disc_item, rather than inheriting directly from Item_base:

      // discount kicks in when a specified number of copies of same book are sold      // the discount is expressed as a fraction to use to reduce the normal price      class Bulk_item : public Disc_item {      public:          Bulk_item(const std::string& book = "",                    double sales_price = 0.0,                    std::size_t qty = 0, double disc_rate = 0.0):               Disc_item(book, sales_price, qty, disc_rate) { }          // redefines base version so as to implement bulk purchase discount policy          double net_price(std::size_t) const;      }; 

The Bulk_item class now has a direct base class, Disc_item, and an indirect base class, Item_base. Each Bulk_item object has three subobjects: an (empty) Bulk_item part and a Disc_item subobject, which in turn has an Item_base base subobject.

Even though Bulk_item has no data members of its own, it defines a constructor in order to obtain values to use to initialize its inherited members.

A derived constructor may initialize only its immediate base class. Naming Item_base in the Bulk_item constructor initializer would be an error.

Key Concept: Respecting the Base-Class Interface

The reason that a constructor can initialize only its immediate base class is that each class defines its own interface. When we define Disc_item, we specify how to initialize a Disc_item by defining its constructors. Once a class has defined its interface, all interactions with objects of that class should be through that interface, even when those objects are part of a derived object.

For similar reasons, derived-class constructors may not initialize and should not assign to the members of its base class. When those members are public or protected, a derived constructor could assign values to its base class members inside the constructor body. However, doing so would violate the interface of the base. Derived classes should respect the initialization intent of their base classes by using constructors rather than assigning to these members in the body of the constructor.


15.4.3. Copy Control and Inheritance

Like any other class, a derived class may use the synthesized copy-control members described in Chapter 13. The synthesized operations copy, assign, or destroy the base-class part of the object along with the members of the derived part. The base part is copied, assigned, or destroyed by using the base class' copy constructor, assignment operator, or destructor.

Exercises Section 15.4.2

Exercise 15.14:

Redefine the Bulk_item and Item_base classes so that they each need to define only a single constructor.

Exercise 15.15:

Identify the base- and derived-class constructors for the library class hierarchy described in the first exercise on page 575.

Exercise 15.16:

Given the following base class definition,

      struct Base {          Base(int val): id(val) { }      protected:          int id;      }; 

explain why each of the following constructors is illegal.

      (a) struct C1 : public Base {              C1(int val): id(val) { }          };      (b) struct C2 : public              C1 { C2(int val): Base(val), C1(val){ }          };      (c) struct C3 : public              C1 { C3(int val): Base(val) { }          };      (d) struct C4 : public Base {              C4(int val) : Base(id + val){ }          };      (e) struct C5 : public Base {              C5() { }          }; 


Whether a class needs to define the copy-control members depends entirely on the class' own direct members. A base class might define its own copy control while the derived uses the synthesized versions or vice versa.

Classes that contain only data members of class type or built-in types other than pointers usually can use the synthesized operations. No special control is required to copy, assign, or destroy such members. Classes with pointer members often need to define their own copy control to manage these members.

Our Item_base class and its derived classes can use the synthesized versions of the copy-control operations. When a Bulk_item is copied, the (synthesized) copy constructor for Item_base is invoked to copy the isbn and price members. The isbn member is copied by using the string copy constructor; the price member is copied directly. Once the base part is copied, then the derived part is copied. Both members of Bulk_item are doubles, and these members are copied directly. The assignment operator and destructor are handled similarly.

Defining a Derived Copy Constructor

If a derived class explicitly defines its own copy constructor or assignment operator, that definition completely overrides the defaults. The copy constructor and assignment operator for inherited classes are responsible for copying or assigning their base-class components as well as the members in the class itself.



If a derived class defines its own copy constructor, that copy constructor usually should explicitly use the base-class copy constructor to initialize the base part of the object:

      class Base { /* ... */ };      class Derived: public Base {      public:          // Base::Base(const Base&) not invoked automatically          Derived(const Derived& d):               Base(d) /* other member initialization */ { /*... */ }      }; 

The initializer Base(d) converts (Section 15.3, p. 577) the derived object, d, to a reference to its base part and invokes the base-class copy constructor. Had the initializer for the base class been omitted,

      // probably incorrect definition of the Derived copy constructor      Derived(const Derived& d) /* derived member initizations */                                    {/* ... */ } 

the effect would be to run the Base default constructor to initialize the base part of the object. Assuming that the initialization of the Derived members copied the corresponding elements from d, then the newly constructed object would be oddly configured: Its Base part would hold default values, while its Derived members would be copies of another object.

Derived-Class Assignment Operator

As usual, the assignment operator is similar to the copy constructor: If the derived class defines its own assignment operator, then that operator must assign the base part explicitly:

      // Base::operator=(const Base&) not invoked automatically      Derived &Derived::operator=(const Derived &rhs)      {         if (this != &rhs) {             Base::operator=(rhs); // assigns the base part             // do whatever needed to clean up the old value in the derived part             // assign the members from the derived         }         return *this;      } 

The assignment operator must, as always, guard against self-assignment. Assuming the left- and right-hand operands differ, then we call the Base class assignment operator to assign the base-class portion. That operator might be defined by the class or it might be the synthesized assignment operator. It doesn't matterwe can call it directly. The base-class operator will free the old value in the base part of the left-hand operand and will assign the new values from rhs. Once that operator finishes, we continue doing whatever is needed to assign the members in the derived class.

Derived-Class Destructor

The destructor works differently from the copy constructor and assignment operator: The derived destructor is never responsible for destroying the members of its base objects. The compiler always implicitly invokes the destructor for the base part of a derived object. Each destructor does only what is necessary to clean up its own members:

      class Derived: public Base {      public:          // Base::~Base invoked automatically          ~Derived()    { /* 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, walking back up the inheritance hierarchy.

15.4.4. Virtual Destructors

The fact that destructors for the base parts are invoked automatically has an important consequence for the design of base classes.

When we delete a pointer that points to a dynamically allocated object, the destructor is run to clean up the object before the memory for that object is freed. When dealing with objects in an inheritance hierarchy, it is possible that the static type of the pointer might differ from the dynamic type of the object that is being deleted. We might delete a pointer to the base type that actually points to a derived object.

If we delete a pointer to base, then the base-class destructor is run and the members of the base are cleaned up. If the object really is a derived type, then the behavior is undefined. To ensure that the proper destructor is run, the destructor must be virtual in the base class:

      class Item_base {      public:          // no work, but virtual destructor needed          // if base pointer that points to a derived object is ever deleted          virtual ~Item_base() { }      }; 

If the destructor is virtual, then when it is invoked through a pointer, which destructor is run will vary depending on the type of the object to which the pointer points:

      Item_base *itemP = new Item_base; // same static and dynamic type      delete itemP;          // ok: destructor for Item_base called      itemP = new Bulk_item; // ok: static and dynamic types differ      delete itemP;          // ok: destructor for Bulk_item called 

Like other virtual functions, the virtual nature of the destructor is inherited. Therefore, if the destructor in the root class of the hierarchy is virtual, then the derived destructors will be virtual as well. A derived destructor will be virtual whether the class explicitly defines its destructor or uses the synthesized destructor.

Destructors for base classes are an important exception to the Rule of Three (Section 13.3, p. 485). That rule says that if a class needs a destructor, then the class almost surely needs the other copy-control members. 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 is not an indication that the assignment operator or copy constructor is also needed.

The root class of an inheritance hierarchy should define a virtual destructor even if the destructor has no work to do.



Constructors and Assignment Are Not Virtual

Of the copy-control members, only the destructor should be defined as virtual. Constructors cannot be defined as virtual. Constructors are run before the object is fully constructed. While the constructor is running, the object's dynamic type is not complete.

Although we can define a virtual operator= member function in the base class, doing so does not affect the assignment operators used in the derived classes. Each class has its own assignment operator. The assignment operator in a derived class has a parameter that has the same type as the class itself. That type must differ from the parameter type for the assignment operator in any other class in the hierarchy.

Making the assignment operator virtual is likely to be confusing because a virtual function must have the same parameter type in base and derived classes. The base-class assignment operator has a parameter that is a reference to its own class type. If that operator is virtual, then each class gets a virtual member that defines an operator= that takes a base object. But this operator is not the same as the assignment operator for the derived class.

Making the class assignment operator virtual is likely to be confusing and unlikely to be useful.



Exercises Section 15.4.4

Exercise 15.17:

Describe the conditions under which a class should have a virtual destructor.

Exercise 15.18:

What operations must a virtual destructor perform?

Exercise 15.19:

What if anything is likely to be incorrect about this class definition?

[View full width]

class AbstractObject { public: virtual void doit(); // other members not including any of the copy-control functions };

Exercise 15.20:

Recalling the exercise from Section 13.3 (p. 487) in which you wrote a class whose copy-control members printed a message, add print statements to the constructors of the Item_base and Bulk_item classes. Define the copy-control members to do the same job as the synthesized versions but that also print a message. Now write programs using objects and functions that use the Item_base types. In each case, predict what objects will be created and destroyed and compare your predictions with what your programs generate. Continue experimenting until you can correctly predict which copy-control members are executed for a given bit of code.


15.4.5. Virtuals in Constructors and Destructors

A derived object is constructed by first running a base-class constructor to initialize the base part of the object. While the base-class constructor is executing, the derived part of the object is uninitialized. In effect, the object is not yet a derived object.

When a derived object is destroyed, its derived part is destroyed first, and then its base parts are destroyed in the reverse order of how they were constructed.

In both cases, while a constructor or destructor is running, the object is incomplete. To accommodate this incompleteness, the compiler treats the object as if its type changes during construction or destruction. Inside a base-class constructor or destructor, a derived object is treated as if it were an object of the base type.

The type of an object during construction and destruction affects the binding of virtual functions.

If a virtual is called from inside a constructor or destructor, then the version that is run is the one defined for the type of the constructor or destructor itself.



This binding applies to a virtual whether the virtual is called directly by the constructor (or destructor) or is called indirectly from a function that the constructor (or destructor) called.

To understand this behavior, consider what would happen if the derived-class version of a virtual function were called from a base-class constructor (or destructor). The derived version of the virtual probably accesses members of the derived object. After all, if the derived-class version didn't need to use members from the derived object, the derived class could probably use the definition from the base class. However, the members of the derived part of the object aren't initialized while the base constructor (or destructor) is running. In practice, if such access were allowed, the program would probably crash.



C++ Primer
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2006
Pages: 223
Authors: Stephen Prata

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net