virtualキーワードについて

@ さんが

と言っておられたので、そういえば、なんで明示的にvirtualを指定させるんだろう、と思ってしまった。

ちなみに、ボクのvirtualキーワードの理解は、派生クラスでオーバーライドするだけならば、付ける必要はないけど、基底クラスの参照もしくはポインタ経由で使いたい場合にvirtualを付けておかないと、動的に解決してくれない、と言うもの。

#include <iostream>

class Base 
{
public:
  Base(){}
  virtual void call() { std::cout << "Base: Virtual" << std::endl; }
  void call2() { std::cout << "Base: Not Virtual" << std::endl; }
};

class Derived : public Base
{
public:
  Derived(){}
  void call() { std::cout << "Derived: Virtual" << std::endl; }
  void call2() { std::cout << "Derived: Not Virtual" << std::endl; }
};

int main(int argc, char** argv)
{
  Base* base = new Derived();
  Derived derived;
  
  base->call();         // "Derived"と表示
  base->call2();       // "Base"と表示 virtualキーワードがないもんね。-- (1)
  (static_cast<Derived*>(base))->call2();   // "Derived"と表示
  
  derived.call();         // "Derived"と表示
  derived.call2();       // "Derived"と表示    -- (2)
  
  delete base;
  
  return 0;
}

昔のボクは、(2)の結果を見て「あれ、virtual付けなくってもオーバーライド出来てるじゃん」と思っていました。そりゃ、Derived型で定義しているオブジェクトのメソッドを呼んでいるんだから、当たり前なんだけど、当時は「じゃぁ、なぜvirtualが必要?」と思っていました。

未だに人に説明できるほど理解はしていないのですが、virtualをつけると、コンパイラがvtblにメソッドへのアドレスを登録してくれるものだ、と思っています。

実際、C++ D&E本のp.94あたりの図を見ると、上記のコードで、クラスDerivedのオブジェクトを作ると、

vptr -----> vtbl:
        &Derived::call()

となるとなっています。そして、このオブジェクトへのポインタがbaseに格納されることになります。

さて、では、baseポインタからcallメソッドを呼び出すと、

base->call();

と言う呼び出しが、実際には

(*(base->vptr[]))(base); 

となって、vptr, vtbl経由でDerived::call()が呼び出されると言う事のようです。

それでは、call2メソッドを呼んだ場合は? この場合は、virtual宣言をしていないので、vptr経由で呼ばれずに、Baseのcall2メソッドが呼ばれてしまう、ということになるようです。

結局

やはり未だによく理解出来てないような気がします。もう一度、このあたり納得できるまで勉強しないといけないですね。

ところで

最初のvitualキーワードはなぜ必要?と言う話ですが、D&E本にも特に書いていません。しかし、 @ さんの

を見ていると、実行時にポインタ経由で動的にメソッドをコールするのは、コストかかるよね。だからデフォルトはvtbl経由じゃなく直接アクセスするようにしたんだよ。それでもポリモーフィズムを使いたいなら、明確にvirtualを付けてよね!!
と言うことになるのだろうか。

そもそもC++が作られた20年前と言うと、想像できないぐらいのpoorなHW環境だったろうし、その時代にはコストのかかるvtbl経由での多相性と言うのは受け入れられなかったのかもしれませんね。でも、Mr.Sとしてはどうしても基底クラス経由での多相性を実現したくて、virtualとキーワードを追加した。。。。のかなぁ。。。