[ Team LiB ] |
![]() ![]() |
Gotcha #54: Copy Constructor Base InitializationHere 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 ] |
![]() ![]() |