共変型の戻り値とオーバーライドルールの緩和

C++では、一般的に戻り値だけが異なるメソッドをオーバーライドすることはできません。

引数が違えば別関数と扱われ、単にメソッドが増えるだけなのですが、引数が同じ場合はオーバーライド対象なので、このコードはコンパイルエラーとなります。

ただし、戻り値の型が異なっていても、その型が共変型であるならばコンパイルエラーにはなりません。

オーバーライドする仮想関数の戻り値の型は、オーバーライド元の関数のものと同じか、または「共変(covariant)」でなければならない。

__C++ランゲージクイックリファレンス

……聞き慣れないですね。

要するに「戻り値の型がクラス型の参照かポインタで、かつis-a関係にあるもの」に関しては親クラスと異なる型の戻り値であってもオーバーライドできますよっていう話です。

例を見てみましょう。

何はともあれ、まずはis-a関係を持つ型を用意します。

先ほどのエラーになったオーバーライドの例の戻り値に今定義した型ShapeとBoxを指定します。

コンパイルは通りますが、共変型を持つメソッドはオーバーライドの挙動が通常のオーバーライドと異なるので注意が必要です。

以下のプログラムでは結果として”shape”と表示されます。

Wandboxで確認

Boxと表示されそうな気がしますが、実際にはshapeと表示されます。

ただし、ここが超重要なのですが、Shape*を返してもb->get()は決してBase::get()の呼び出しでは無いということです。

ポリモーフィズムの挙動に準じて、あくまでDerived::get()が呼び出されますが、操作しているポインタbの型次第で、Derived::get()で指定した型Box*のまま戻り値が返されるか、Base::get()で指定した型に暗黙に変換された後、返されるかという挙動の違いが発生します。

慣れないとちょっと分かりづらいですね。

むしろ結構分かりづらいですね。

ひょっとしたらC++の挙動の中でトップレベルの分かりづらさかもしれないですね。

 

一応、共変型に対してオーバーライドのルールが緩和される為、純粋仮想関数による子クラスへの関数定義の強制のメリットと実際に取得する型が親クラスに引っ張られないメリットを享受できる……ということになっています。

自分自身の型を返す関数を定義する時など、親クラス固定だと、受け取った側でのキャストが面倒ですしね。

共変型の戻り値には、常に適切な抽象レベルで作業できるという利点があります。(中略)共変型の戻り値を使用すれば、ミスを誘いがちなキャストを使用したために、そもそも失うはずの無かった型情報を再度提供するはめになることもありません。

__C++標準的コーディング技法#31 戻り値の共変型

 

まとめ

  1. 戻り値の型が違うメソッドはオーバーライドできません
  2. ただし共変型の場合のみこのルールが緩和される
  3. 操作している型のタイプによって戻り値の型は暗黙に変換される
  4. 型が暗黙に変換されるだけで関数の呼び出し自体はポリモーフィック
Pocket