Declarations: classes and class members (C++)

Rule: in a class, declare the public section first, followed by the protected section and finally by the private section.

Rule: make sure that a class defines at least a constructor, the copy constructor, the assignment operator or the destructor. If the compiler generated version of any of the above is acceptable for the class, write a short comment to acknowledge that.

Rule: the assignment operator is to check and avoid assignment of an object to itself.



The declaration of a class is read mostly by users of that class, who are mainly interested in the public interface. The protected and private interfaces are only of interest to programmers who want to inherit from the class or have to work on the class itself (e.g. for maintenance). So the public section should be declared first, where it is easier to find, followed by the protected and private sections.

Rule: in a class, declare the public section first, followed by the protected section and finally by the private section.

When a class is declared, it always has at least four methods: a default constructor, the destructor, the copy constructor and the assignment operator. Even if you don't declare one or more of these methods explicitly, the compiler automatically provides those that are missing. So for example if you declare:

    class User
    {
    public:
        // Default constructor
        User();

        // Constructor
        User( int id, const char * name );

        // Destructor
        ~User();

    private:
        int id_;
        char * name_;
    };

    User::User() : id_(0), name_(0) 
    {
    }

    User::User( int id, const char * name ) : id_( id ), name_( 0 )
    {
        if( name != 0 ) {
            name_ = new char [ strlen(name_) ];
            strcpy( name_, name );
        } 
    }

    User::~User()
    {
        delete [] name_;
    }

you actually get the following:

    class User
    {
    public:
        // Default constructor
        User();

        // Constructor
        User( int id, const char * name );

        // Destructor
        ~User();
 
        // Copy constructor (provided by compiler)
        User( const User & u );

        // Assignment operator (provided by compiler)
        User & operator = ( const User & u );

    private:
        int id_;
        char * name_;
    };

Here the compiler implicitly adds two public methods to the class, so it is possible to write:

    User * u = new User( 1234, "Hello" );
    User * w = new User( u ); // Uses the copy constructor!
    User z;

    z = u; // Uses the assignment operator!

even if we haven't declared the copy constructor or the assignment operator explicitly.

The compiler provides a very simple implementation for these methods: the copy constructor performs a bit-by-bit copy of the entire object, while the assignment operator calls in turn the assignment operator associated to (the type of) each member variable. In practice, the implementation of these methods would look quite similar to:

    User::User( const User & u ) {
        memcpy( this, &u, sizeof(User) );
    }

    User & operator = ( const User & u ) {
        id_ = u.id_;
        name_ = u.name_;
        return *this;
    }

Although the compiler is not to blame (it just follows the language manual), this strategy is particularly weak and dangerous when the object contains pointers to dynamic memory. In the example above both w->name_ and z.name_ will eventually come out with the same value as u->name_, so when one of these is deleted (say by the object destructor) the remaining two become invalid pointers to deallocated memory:

    delete u; // Ok, but destructor deletes member u->name_ too
    delete w; // Danger! w->name_ has same value of as u->name_, which is already destroyed!

There are two possibilities here, that is either to provide the correct implementation or to remove the wrong one. In the first case we write additional code so that when the object is copied the memory pointed at by the variable name_ is also copied:

    User::User( const User & u ) : id_( u.id ), name_( 0 )
    {
        if( u.name_ != 0 ) {
            // Copy name_ in a new memory block
            name_ = new char [ strlen(u.name_) ];
            strcpy( name_, u.name_ );
        } 
    }
    
    User & User::operator = ( const User & u )
    {
        if( this != &u ) {
             id_ = u.id_;

            delete [] name_;
            name_ = 0;

            if( u.name_ != 0 ) {
                // Copy name_ in a new memory block
                name_ = new char [ strlen(u.name_) ];
                strcpy( name_, u.name_ );
            }

            return *this;
        }
    }

The implementation above is just an example, however it does ensure that the member variable name_ gets correctly assigned. To remove the methods altogether, the best method is to declare them in the private section with no implementation, as this will cause an error both in the compiler and linker.

    class User
    {
    public:
        // ...omitted, see above...
        // Default constructor
        User();

        // Constructor
        User( int id, const char * name );

        // Destructor
        ~User();

    private:
        // Copy constructor and assignment operator not allowed for this class!
        User( const User & u );
        User & operator = ( const User & );

        int id_;
        char * name_;
    };

It is also well possible that the compiler generated methods are perfectly good for the class at hand, but even in this case it is good practice to add a short comment in that regard:

    class Point
    {
    public:
        Point( int x, int y );

        // Note: it's ok to use default copy constructor and assignment operator

        // ...

    private:
        int x_;
        int y_;
    };

Rule: make sure that a class defines at least a constructor, the copy constructor, the assignment operator or the destructor. If the compiler generated version of any of the above is acceptable for the class, write a short comment to acknowledge that.

It is important that the assignment operator is safe from self assignments (of the type a = a) because while this situation is not frequent in practice, it has the potential of resulting in a class of bugs that may be quite difficult to find. This can especially happen when some member variable is deleted or some other deinitialization or side effect takes place.

See the class User above for the typical implementation of this check, and consider what happens if we remove it and assign the object to itself:

    // Warning: assignment operator does not check for
    // assignment to self
    User & User::operator = ( const User & u )
    {
        id_ = u.id_;

        delete [] name_;
        name_ = 0;

        if( u.name_ != 0 ) {
            // Copy name_ in a new memory block
            name_ = new char [ strlen(u.name_) ];
            strcpy( name_, u.name_ );
        }

        return *this;
    }

    ...

    User w;

    w = w;

when name_ is deleted and reset to the null pointer, u.name_ is also deleted and reset because u is the same object. So at the end of the function the member variable name_ is 0 and its previous content has been lost... not exactly what you expect by an operation that should perform about nothing!

Rule: the assignment operator is to check and avoid assignment of an object to itself.

Top: Table of content  |  Previous: Declarations: variables  |  Next: Expressions

Copyright (c) 2003 Alessandro Scotti. All rights reserved.