#7 ポリモーフィズムのための基底クラスには仮想デストラクタを宣言する.

Effective C++ 2章7項の内容です.

1.仮想関数

そもそも仮想関数とは何か?という話ですが,クラスのメンバ関数として宣言するときに,関数の前にvirtualをつけてあげれば仮想関数になります.で,なんでこんなことするか?ですが,下記の状況で使いたくなる時があります.

1.基底クラスに定義したメソッドを派生クラスでオーバーライドしている.
2.派生クラスを基底クラスのポインタとして扱っている.
3.基底クラスのポインタとして扱っていながらも,実際のオブジェクト(派生クラス)の振る舞いをしてほしい.(ポリモーフィズム

1−1.virtualなしで定義した場合.

クラスの定義として,下記の2つがあるとします.

class HumanWoVirtual {
public:
  ~HumanWoVirtual() { std::cout << "~HumanWoVirtual()" << std::endl; };
  void say() { std::cout << "I am HumanWoVirtual!" << std::endl; };
};

class ManWoVirtual : public HumanWoVirtual {
public:
  ~ManWoVirtual() { std::cout << "~ManWoVirtual()" << std::endl; };
  void say() { std::cout << "I am ManWoVirtual!" << std::endl; };
};

ここで,基底クラス,派生クラスの両方にsay()という関数が定義されていますが,virtualがついていません.そのため,ポリモーフィズムを期待して下記のようなコードを書いても,意図した挙動になりません.

  // ポインタとしてオブジェクトを扱う場合,振る舞いが異なる.
  // virtualとして関数が定義されている場合,ポインタが基底クラスの型になっていても,実体の関数が呼ばれる.
  {
    HumanWoVirtual *manWoVirtual = new ManWoVirtual;
    std::cout << std::endl;
    std::cout << "manWoVirtual->say()" << std::endl;
    manWoVirtual->say();
    delete manWoVirtual;
  }

  {
    Human *man = new Man();
    std::cout << std::endl;
    std::cout << "man->say()" << std::endl;
    man->say();
    delete man;
  }

実行結果は,下記のようになります.見てわかる通り,アップキャストした側に定義されているsayが実行されています.これが望む挙動ならいいのかもしれませんが,「Humanの型として表現されていても,中身は実際にはManだから,Manのsayが呼ばれてほしい!」という場合には,say()の関数の頭にvirtualをつけてあげれば望む挙動に変わります.

manWoVirtual->say()
I am HumanWoVirtual! <- ポインタ先に格納されている実体は ManWoVirtual であるにもかかわらず,HumanWoVirtual.say() が呼ばれている.
~HumanWoVirtual()  <- ポインタ先に格納されている実体は ManWoVirtual であるにもかかわらず,~HumanWoVirtual() が呼ばれている.

1−2.virtualありで定義した場合.

クラスの定義を少しだけ変えました.

class Human {
  public:
  virtual ~Human() { std::cout << "~Human()" << std::endl; };
  virtual void say() { std::cout << "I am Human!" << std::endl; };
};

class Man : public Human {
  public:
  virtual ~Man() { std::cout << "~Man()" << std::endl; };
  virtual void say() { std::cout << "I am Man!" << std::endl; };
};

すると,実行結果が下記のように変わりました.ここで,一点注目するところとしては,static_castで実体をアップキャストした場合,結局コピーコンストラクタが呼ばれて,アップキャスト側のオブジェクトが実際に生成されます.そのため,”static_cast(manWoVirtual).say();”の出力結果としては,結局HumanWoVirtual::say()隣ります.

man->say()
I am Man!  <- 基底クラスのポインタで呼ばれても,きちんと派生クラスの say() が呼ばれている.
~Man()    <- 基底クラスのポインタで呼ばれても,きちんと派生クラスのデストラクタが呼ばれている.
~Human()

man.say()
I am Human!   <- static_castすると,アップキャスト側のオブジェクトが実際に生成されるので,Human.say() が呼ばれる.
~Human()       <- static_castで生成されたコピーに対するデストラクタのコール.
~Man() 
~Human()

2.仮想デストラクタを宣言する必要性.

なんだか本題がおまけみたいな扱いになってしまいましたが,ここまでの話をデストラクタの観点で考えると,基底クラスのデストラクタにvirtualをつけておかないと,派生クラスが基底クラスのポインタとして扱われるときに派生クラスのデストラクタが呼ばれず,メモリがリークしてしまう可能性があるために,基底クラスになりうるクラスのデストラクタには virtual をつけましょう.という話でした!