C++ ではスライシングと呼ばれている現象があります。 これは派生クラスのオブジェクトが基底クラスに型変換したときに情報が切り詰められるというものです。 特に仮想クラスを継承したクラスにおいて問題を生じやすいということが知られています。
端的に説明した記事が以前に投稿されているので、これを見ればどういう現象なのかよくわかります。
》》 C++のスライシング
仮想クラスを必要とする状況というのは、その仮想クラスを継承するクラスがインターフェイスは同じで挙動は変えたいという場合です。 それなのに派生クラスで定義したメンバ関数が全て消えて基底クラスのふるまいをするようになるのは困ったことです。
こうなるのは実際に生成されるコードを考えれば理解できる話ではあるのです。 仮想クラスのオブジェクトは一般的に仮想関数テーブルへのポインタを持つように実装され、その仮想クラスから派生したクラスのオブジェクトは別の仮想関数テーブルへのポインタを持ちます。 派生クラスで仮想メンバ関数を上書きしたメンバ関数は派生クラスのデータメンバにアクセスする可能性があるので基底クラスに型変換したときに消えてしまうデータメンバへアクセスしようとすると辻褄が合わなくなります。 (仮想クラスである) 基底クラスに型変換するときは仮想関数テーブルも基底クラスのものに置き換えざるを得ません。
可能であればスライシングが起こっても辻褄が合うように定義できるのならば望ましいですし、出来ないのならばスライシングしないように扱えば問題は起こりませんが、スライシングできないように定義するのは良い考えです。 必要な機能を付けるだけでなく使ってはいけないときに使えないようにすることの重要性を私はしばしば述べていますが、今回もその一環です。
元記事のコードを改良したものを書いてみました。 基底クラスのコンストラクタや代入演算子を削除することで問題のある個所がエラーにできたようです。 コンストラクタなどに default
や delete
を指定できるのは C++11 からの機能ですが、それ以前でも private
指定することで delete
したのと同じような効果を得ることはできます。
#include <iostream>
struct Base {
virtual void print() const {
std::cout << "Base" << std::endl;
}
Base()=default;
Base(Base& x)=default;
template<class T> Base(T& x)=delete;
Base & operator=(Base const &)=default;
template<class T> Base & operator=(T const &)=delete;
};
struct Derived : public Base {
public:
void print() const {
std::cout << "Derived" << std::endl;
}
};
void print_val(Base v) {
v.print();
}
void print_ref(Base& r) {
r.print();
}
int main(void) {
Base base;
Derived derived;
print_val(base); //=> Base
print_val(derived); //=> ここがエラーになる。 この行をコメントアウトするとコンパイル可能
print_ref(base); //=> Base
print_ref(derived); //=> Derived
return 0;
}