Effective C++ by Scott Meyers (Short Summary)
  • Introduction
  • Chapter 1 - Accustoming Yourself to C++
    • Item 1 - View C++ as a federation of languages.
    • Item 2 - Prefer consts, enums, and inlines to #defines.
    • Item 3 - Use const whenever possible.
    • Item 4 - Make sure that objects are initialized before they’re used.
  • Chapter 2 - Constructors, Destructors, and Assignment Operators
    • Item 5 - Know what functions C++ silently writes a calls.
    • Item 6 - Explicitly disallow the use of compiler-generated functions you do not want.
    • Item 7 - Declare destructors virtual in polymorphic base classes.
    • Item 8 - Prevent exceptions from leaving destructors.
    • Item 9 - Never call virtual functions during construction or destruction.
    • Item 10 - Have assignment operators return a reference to *this.
    • Item 11 - Handle assignment to self in operator=.
    • Item 12 - Copy all parts of an object.
  • Chapter 3 - Resource Management
    • Item 13 - Use objects to manage resources.
    • Item 14 - Think carefully about copying behavior in resource-managing classes.
    • Item 15 - Provide access to raw resources in resource-managing classes.
    • Item 16 - Use the same form in corresponding uses of new and delete.
    • Item 17 - Store newed objects in smart pointers in standalone statements.
  • Chapter 4 - Designs and Declarations
    • Item 18 - Make interfaces easy to use correctly and hard to use incorrectly.
    • Item 19 - Treat class design as type design.
    • Item 20 - Prefer pass-by-reference-to-const to pass-by-value.
    • Item 21 - Don't try to return a reference when you must return an object.
    • Item 22 - Declare data members private.
    • Item 23 - Prefer non-member non-friend functions to member functions.
    • Item 24 - Declare non-member functions when type conversions should apply to all parameters.
    • Item 25 - Consider support for a non-throwing swap.
Powered by GitBook
On this page
  1. Chapter 2 - Constructors, Destructors, and Assignment Operators

Item 9 - Never call virtual functions during construction or destruction.

Suppose you've got a class hierarchy for modelling stock transactions, e.g., buy order, sell orders, etc. It's important that such transaction be auditable, so each time a transaction object is created, an appropriate entry needs to be created in an audit log. This seems like a reasonable way to approach the problem:

class Transaction {                             // base class for all transactions.
public:
    Transaction();
    virtual void logTransaction() const = 0;    // make type-dependent log entry
    ...
};
Transaction::Transaction()                    // implementation of base class ctor
{
    ...
    logTransaction();                        // as final action, log this transaction
}

class BuyTransaction: public Transaction    // derived class
{
public:
    virtual void logTransaction() const;    // how to log transactions of this type
    ...
};

class SellTransaction: public Transaction    // derived class
{
public:
    virtual void logTransaction() const;    // how to log transactions of this type
    ...
}

Consider what happens when this code is executed:

BuyTransaction b;

Clearly a BuyTransaction constructor will be called, but first, a Transaction constructor must be called; base class parts of derived class objects are constructed before derived class parts are. The last line of the Transaction constructor calls the virtual function logTransaction, but there is where the surprise comes in. The version of logTransaction that's called is the one in Transaction, not the one in BuyTransaction — even though the type of object being created is BuyTransaction. During base class construction, virtual functions never go down into derived classes.

In our example, while the Transaction constructor is running to initialize the base class part of a BuyTransaction object, the object is of type Transaction. That's how every part of C++ will treat it, and the treatment makes sense: the BuyTransaction specific parts of the object haven't been initialized yet, so it's safest to treat them as if they didn't exist.

An object doesn't become a derived class object until execution of a derived class constructor begins.

They same reasoning applies during destruction. Once a derived class destructor has run, the object's derived class data members assume undefined values, so C++ treats them as if they no longer exist. Upon entry to the base class destructor, the object becomes a base class object, and all parts of C++ — virtual functions, dynamic_cast, etc., — treat it that way.

It's not always so easy to detect calls to virtual functions during construction or destruction. if Transaction had multiple constructors, each of which had to perform some of the same work, it would be good software engineering to avoid code replication by putting the common initialization code, including the call to logTransaction, into a private non-virtual initialization function, say, init:

class Transaction {
public:
    Transaction()
    { init(); }                                // call to non-virtual
    virtual void logTransaction() const = 0;
    ...
private:
    void init()
    {
         ...
         logTransaction();                    // that calls a virtual!
     }
 };

This code is conceptually the same as the earlier version, but it's more insidious, because it will typically compile and link without complaint. In this case, because logTransaction is pure virtual in Transacction, most runtime systems will abort the program when the pure virtual is called. However, if logTransaction were a normal virtual function (i.e. not pure virtual) with an implementation in Transaction, that version would be called, and the program would merrily trot along, leaving you to figure out why the wrong version of logTransaction was called when a derived class object was created.

Reasonable Solution:

There are different ways to approach this problem. One is to turn logTransaction into a non-virtual function in Transaction, then require that derived class constructors pass the necessary log information to the Transaction constructor. That function can then safely call the non-virtual logTransaction. Like this:

class Transaction {
public:
    explicit Transaction( const std::string& logInfo);
    void logTransaction( const std::string& logInfo) const;    // now a non-virtual function
    ...
};
Transaction::Transaction( const std::string& logInfo)
{
    ...
    logTransaction(logInfo)                                    // now a non-virtual call
}
class BuyTransaction: public Transaction {
public:
    BuyTransaction(parameters): 
        Transaction( createLogString(parameters))             // passing log info to base class constructor
    { ... }
    ...
private:
    static std::string createLogString(parameters);
};

In other words, since you can't use virtual functions to call down from base classes during construction, you can compensate by having derived classes pass necessary construction information up to base class constructors instead.

In this example, note the use of the private static function createLogString in BuyTransaction. Using the helper function to create a value to pass to a base class constructor is often more convenient than going through contortions in the member initialization list ot give the base class what it needs.

Things to Remember:

  • Don't call virtual functions during construction or destruction, because such calls will never go to a more derived class than that of the currently executing constructor or destructor.

PreviousItem 8 - Prevent exceptions from leaving destructors.NextItem 10 - Have assignment operators return a reference to *this.

Last updated 3 years ago