EffectiveJavaを読んでて、C++でも当てはまるのかな?と思って試してみたら当てはまったので…

まとめ

  • オーバーライドされたvirtualなメソッド呼び出しは実行時に決定
    • 実態が子クラスなら、親クラスとして扱っても子クラスのメソッドが実行される
    • 実態が孫クラスの場合は孫クラスのメソッドが呼ばれる
    • 親から子、孫へと呼び出される関数が探されていく感じ
  • オーバーロードされたメソッド呼び出しはコンパイル時に決定
    • virtual関数のように実態で切り替えが出来ない
    • 実態が子クラスでも、親クラスとして扱うなら親クラスのメソッドが実行される
    • 一致するものが無い場合は親クラスにキャストして一致するものを探す
    • 子から親へと一致するものを探していく感じ

オーバーライドとは

メソッドのオーバーライドとは、親クラスのメソッドを子クラスで再定義することです。 C++では親クラスでvirtualにされているメソッドをオーバーライドすると、 子クラスのオブジェクトを親クラスにキャストしてメソッドを呼び出しても、再定義された子クラスのメソッドが呼ばれます。 (virtualをつけないと親クラスのメソッドが呼ばれます)

たとえば以下のコードでは、オーバーライドされているためExtのshowメソッドが実行され、「show ext」が表示されます。

#include <cstdio>

class Base {
public:
  virtual void show() { printf("show base\n"); }
};

class Ext: public Base {
public:
  virtual void show() { printf("show ext\n"); }
};

int main() {
    Ext *e = new Ext();
    Base *b = static_cast<Base*>(e);
    b->show();
    return 0;
}

このように、オーバーライドされたメソッドは実行時にどのメソッドが呼ばれるか決定されます。

オーバーロードとは

一方、似たような名前でオーバーロードというものもあります。 こちらは継承とは特に関係なく、同じメソッド名で引数の違うものを定義しておくと、 引数の種類をみて自動的に一致するメソッドが実行されます。

たとえば以下の場合、呼び出しているのはprintという名前の関数ですが、 それぞれ引数が一致する関数が呼ばれるため、base、extと表示されます。

#include <cstdio>

class Base { };

class Ext: public Base { };

void print(Base *b) {printf("base\n");}
void print(Ext *b) {printf("ext\n");}

int main() {
    Ext *e = new Ext();
    Base *b = new Base();
    print(e);
    print(b);
    return 0;
}

オーバーロードはコンパイル時決定

オーバーライドを見ているとオーバーロードも実行時にどれを呼び出すか決定していそうですが、 実際はコンパイル時にすでにどれが呼び出されるか決定しています。

そのため、以下のように実態が子クラスだとしても、親クラスとして引数に渡した場合、親クラスのメソッドが呼ばれます。

#include <cstdio>
#include <vector>

class Base {
public:
  virtual void show() { printf("show base\n"); }
};

class Ext: public Base {
public:
  virtual void show() { printf("show ext\n"); }
};

void print(Base *b) {printf("base\n");}
void print(Ext *b) {printf("ext\n");}


int main() {
std::vector<Base*> b;
b.push_back(new Base());
b.push_back(new Ext());

for (int i=0; i < b.size(); ++i){
  print(b.at(i));
  b.at(i)->show();
  }

 return 0;
}

出力は以下の通りです

base
show base
base
show ext

ただし親クラスは検索対象になる

一方でオーバーロードは型が一致するものが無い場合は引数として渡されたクラスの継承ツリーをたどり、一致する親クラスを探してそれにキャストして実行してくれます。 もちろん親クラスから子クラスへはコンパイル時にはわからないため、子クラスから親クラスに検索を行います。 そのため、以下のコードは「ext」と表示されます。

#include <cstdio>
#include <vector>

class Base {
};

class Ext: public Base {
};

class ExtExt: public Ext {
};

void print(Base *b) {printf("base\n");}
void print(Ext *b) {printf("ext\n");}

int main() {
    ExtExt *b = new ExtExt();
    print(b);
    return 0;
}

一意に定まらない場合

なお、本当にどっちか決められない場合はambiguousというエラーになります

#include <cstdio>
#include <vector>

class InterA{
public:
    virtual void interA() = 0;
};

class InterB{
public:
    virtual void interB() = 0;
};

class Base : public InterA, public InterB {
public:
  virtual void interA() { printf("interA\n"); }
  virtual void interB() { printf("interB\n"); }
};


void print(InterA *i) { i->interA(); }
void print(InterB *i) { i->interB(); }

int main() {
    Base *b = new Base();
    print(b);
    return 0;
}
test.cpp:26:5: error: call to 'print' is ambiguous
    print(b);
    ^~~~~
このエントリーをはてなブックマークに追加