22.2.  Polymorphism and virtual Destructors

[ fromfile: inheritance-detail.xml id: virtualdestructors ]

When operating on classes in inheritance hierarchies, we often maintain containers of base class pointers that hold addresses of derived objects. Example 22.3 defines a Bank class that has a container of various kinds of Accounts.

Example 22.3. src/derivation/assigcopy/bank.h

[ . . . . ]
class Account;

class Bank {
 public:
    Bank& operator<< (Account* acct); 1
    ~Bank();
    QString getAcctListing() const;
 private:
    QList<Account*> m_Accounts;
};
[ . . . . ]

1

This is how to add object pointers to m_Accounts.

<include src="src/derivation/assigcopy/bank.h" href="src/derivation/assigcopy/bank.h" mode="cpp" id="bankh"/>


The Account classes are defined in Example 22.4.

Example 22.4. src/derivation/assigcopy/account.h

[ . . . . ]
class Account {
 public:
    Account(unsigned acctNum, double balance, QString owner);
    virtual ~Account(){
      qDebug() << "Closing Acct - sending e-mail " 
               << "to primary acctholder:" << m_Owner; }
    virtual QString getName() const {return m_Owner;}
    // other virtual functions
 private:
    unsigned  m_AcctNum;
    double    m_Balance;
    QString    m_Owner;
};
class JointAccount : public Account {
 public:
  JointAccount (unsigned acctNum, double balance, 
                QString owner, QString jowner);
  JointAccount(const Account & acct, QString jowner);
  ~JointAccount() {
     qDebug() << "Closing Joint Acct - sending e-mail "
              << "to joint acctholder:" << m_JointOwner; }
  QString getName() const { 
    return QString("%1 and %2").arg(Account::getName())
                   .arg(m_JointOwner);
  }
  // other overrides
 private:
  QString m_JointOwner;
};
[ . . . . ]

<include src="src/derivation/assigcopy/account.h" href="src/derivation/assigcopy/account.h" mode="cpp" id="acaccth"/>


Bank can perform uniform operations on its collected Accounts by calling virtual methods on each one. In Example 22.5, delete acct causes an indirect call to the destructor of Account and the subsequent release of allocated memory.

Example 22.5. src/derivation/assigcopy/bank.cpp

[ . . . . ]

#include <QDebug>
#include "bank.h"
#include "account.h"

Bank::~Bank() {
    qDeleteAll(m_Accounts);
    m_Accounts.clear();
}


Bank& Bank::operator<< (Account* acct) {
   m_Accounts << acct;
   return *this;
}

QString Bank::getAcctListing() const {
   QString listing("\n");
   foreach(Account* acct, m_Accounts)  
      listing += QString("%1\n").arg(acct->getName()); 1
   return listing;
}

1

getName() is virtual.

<include src="src/derivation/assigcopy/bank.cpp" mode="cpp" href="src/derivation/assigcopy/bank.cpp" id="bankdtorcpp" segid="dtor"/>


Although every address in the list is an Account, some (perhaps all) might point to derived-class objects and therefore require derived-class destructor calls.

If the destructor is virtual, the compiler enables runtime binding on any destructor call through an Account pointer, instead of simply calling Account::~Account() on each one. Without declaring ~Account() to be virtual in the base class, you would get an incorrect result from running Example 22.6.[123]

Example 22.6. src/derivation/assigcopy/bank.cpp

[ . . . . ]

int main() {
  QString listing;
   {                                                   1
      Bank bnk;
      Account* a1 = new Account(1, 423, "Gene Kelly");
      JointAccount* a2 = new JointAccount(2, 1541, "Fred Astaire",
         "Ginger Rodgers");
      JointAccount* a3 = new JointAccount(*a1, "Leslie Caron");
      bnk << a1;
      bnk << a2;
      bnk << a3;
      JointAccount* a4 = new JointAccount(*a3);        2
      bnk << a4;
      listing = bnk.getAcctListing();
    }                                                  3
    qDebug() << listing;
    qDebug() << "Now exit program" ;
} 

1

Begin internal block.

2

What's this?

3

At this point, all four Accounts are destroyed as part of the destruction of the bank.

<include src="src/derivation/assigcopy/bank.cpp" mode="cpp" href="src/derivation/assigcopy/bank.cpp" id="bankcppmain" segid="main"/>


Following is the output from this program with virtual removed from ~Account.

Closing Acct - sending e-mail to primary acctholder:Gene Kelly
Closing Acct - sending e-mail to primary acctholder:Fred Astaire
Closing Acct - sending e-mail to primary acctholder:Gene Kelly
Closing Acct - sending e-mail to primary acctholder:Gene Kelly
[ ... ]

By making the destructor virtual, both types of Account will get destroyed properly and, in this example, both account holders of a joint account will get proper e-mail notifications when the Bank is destroyed.

Closing Acct - sending e-mail to primary acctholder:Gene Kelly
Closing Joint Acct - sending e-mail to joint acctholder:Ginger Rodgers
Closing Acct - sending e-mail to primary acctholder:Fred Astaire
Closing Joint Acct - sending e-mail to joint acctholder:Leslie Caron
Closing Acct - sending e-mail to primary acctholder:Gene Kelly
Closing Joint Acct - sending e-mail to joint acctholder:Leslie Caron
Closing Acct - sending e-mail to primary acctholder:Gene Kelly
[ ... ]
[Note]Note

If you declare one or more virtual methods in a class, you should define a virtual destructor for that class, even if it has an empty body.



[123] Compilers report a missing virtual in the destructor as a warning, and the behavior is undefined, so you may not see the same thing on your system.