C++ Check List

Synopsis:

This page presents a check list of things to avoid or manage when implementing C++ code. They represent common sources of error and, occasionally, things that might not seem like errors to an un-trained eye. The list is surprisingly short for a language as big and capable as C++.

  1. Compiler Generated Member Functions:

    Every C++ object is created by means of a call to a constructor and destroyed with a destructor. This happens even if the class designer did not provide those functions. If not, the compiler does a member-wise construction of each of the class's bases and instances of composed classes. This compiler generated code may or may not be correct.

    For each class you implement you must decide whether to use the compiler generated functions:

    • default constructor - generated if no constructors are declared
    • copy constructor - always generated if not declared
    • assignment operator - always generated if not declared
    • destructor - always generated if not declared
    • move constructor - generated if copy constructor, move and copy assignment, and destructor are not declared
    • move assignment - generated if move and copy constructor, copy assignment, and destructor are not declared
    or implement them yourself, or delete them.

    If the bases and data members of your class have correct copy, assignment, and destruction semantics, then you should use the compiler generated code. That typically happens when your class has only primitive, non-pointer, members and STL container members

    If that is not the case then you must either implement correct versions of those functions or delete them. If your class has data members that are not copyable and/or assignable, then you should declare the generated functions as deleted.

  2. Constructor Initialization:

    The C++ language guarantees that every instance of a class, as well as primitive data, will be initialized when declared. A class's bases and members are initialized before the body of a class constructor is executed. You can imagine the bases and members being intialized as the thread of execution finishes stack frame setup and before the body of the constructor is entered.

    A class's bases and instances of composed data members should be initialized with constructor initialization sequences. This allows the designer to specify exactly which constructors will be called on its bases and members. This also reduces the number of operations required for construction of the class. So this is both a correctness and a performance issue.

    Also, if the class composes any const or reference members then they must be initialized in the initialization sequence, as they can not be reset in the body of the constructor.

  3. Overloading:

    Overloading should always be limited to a single scope. Never overload a base class function in a derived class. If you do overload across class scopes you will hide all the base overloads. That results in, at best, compilation errors, and worst, subtle errors due to unexpected conversions because the intended function call is hidden.

  4. Overriding:

    There are several potential errors with overriding:

    • Overloading virtual functions in a base class will cause hiding of the overloads when a derived class correctly overrides one of the overloaded virtual functions in the base.
    • Overriding, in a derived class, non-virtual functions in its base. That causes calls made using a base class pointer bound to a derived class instance to invoke the base class function, not the overridden derived class function. This happens because the non-virtual function does not have an entry in the class virtual function pointer table and so cannot be dynamically dispatched.
    • Failing to provide a virtual destructor in a base class causes incomplete destruction when a derived instance is created on the heap and then deleted through a base class pointer. The reason this happens is for the same reason as the previously cited error. There is no virtual function pointer table entry for the destructor and so the derived destructor cannot be dispatched through a base pointer.

  5. Multiple Inheritance:

    If a derived class D inherits from two classes C and D, and C and D each are derived from a common class A, then D will contain multiple definitions for each of its functions and members. This causes build failures unless the methods of D disambiguate each call to inherited A methods by qualifying with B:: or C:: to indicate which of the ambiguous methods should be called.

    The ambiguities can be resolved more directly by using virtual inheritance, resulting in only a single instance of A to be shared by both B and C, eliminating the ambiguity. However, should B need to initialize its A in a way different than that needed by C, then there will be an error since both unique initializations are not possible. That means that correct code in D can break correct code in B and C.