[ Team LiB ] Previous Section Next Section

Gotcha #54: Copy Constructor Base Initialization

Here are a couple of simple components:



class M { 


 public:


   M();


   M( const M & );


   ~M();


   M &operator =( const M & );


   // . . .


};


class B {


 public:


   virtual ~B();


 protected:


   B();


   B( const B & );


   B &operator =( const B & );


   // . . .


};


Let's leverage these components to produce a new class—and try to get the compiler to do as much work for us as is reasonable:



class D : public B { 


   M m_;


};


While class D doesn't inherit constructors, destructor, or the copy assignment operator from its base class, the compiler will write these operations for us implicitly, leveraging the corresponding implementations of the components. (See Gotcha #49.) For example, the compiler's implementation of D's default constructor will be as a public inline member function. The constructor will first invoke the base class B's default constructor, then the default constructor for the M member. The destructor will, as always, do the inverse: it will first destroy the member, then call the base class destructor.

The copy operations are more interesting. The compiler-generated copy constructor will perform a member-by-member initialization, as if we had written it like this:



D::D( const D &init ) 


   : B( init ), m_( init.m_ )


   {}


The compiler-generated assignment operator performs a member-by-member assignment, as if we had written it like this:



D &D::operator =( const D &that ) { 


   B::operator =( that );


   m_ = that.m_;


   return *this;


}


Suppose we add a data member to our class that doesn't define these operations? For example, we could add a data member that points to a heap-allocated X:



class D : public B { 


 public:


   D();


   ~D();


   D( const D & );


   D &operator =( const D & );


 private:


   M m_;


   X *xp_; // new data member


};


Now we should write all these operations explicitly. The default constructor and the destructor are straightforward, and we can let the compiler do most of the work for us:



D::D() 


   : xp_( new X )


   {}


D::~D()


   { delete xp_; }


The compiler invokes the default constructors and destructors for the base class and member m_ implicitly. It's tempting to think we can get away with the same approach when implementing copy construction and copy assignment, but we can't:



D::D( const D &init ) 


   : xp_( new X(*init.xp_) )


   {}


D &D::operator =( const D &rhs ) {


   delete xp_;


   xp_ = new X(*rhs.xp_);


   return *this;


}


Both these implementations will compile without error and do the wrong thing at runtime. Our copy constructor implementation correctly initializes the member xp_ with a copy of what its initializer's xp_ refers to, but the base class and m_ member are initialized using B's and M's default constructors respectively, rather than their copy constructors. In the case of the assignment, the values of the base class part and m_ are unchanged.

Once you take over the job of writing any of these member functions from the compiler, you're responsible for the entire implementation:



D::D( const D &init ) 


   : B( init ), m_( init.m_ ), xp_( new X(*init.xp_) )


   {}


D &D::operator =( const D &rhs ) {


   if( this != &rhs ) {


       B::operator =( rhs );


       m_ = rhs.m_;


       delete xp_;


       xp_ = new X(*rhs.xp_);


   }


   return *this;


}


This is the case for the default constructor and destructor as well, but the implicit invocation of the default constructors for the base class and m_ member resulted in correct code in that case. I prefer the approach that minimizes typing, but if you prefer, you can be explicit:



D::D() 


   : B(), m_(), xp_( new X )


   {}


    [ Team LiB ] Previous Section Next Section