オブジェクトスライシングは、C++ の一般的なポリモーフィズムの問題です。派生クラスのオブジェクトを基本クラスに代入すると、派生部分は切り捨てられ、基本クラスのオブジェクト部分のみが保持されるため、予期しない結果が生じることがあります。
オブジェクトスライシングの例#
次のコードを考えてみましょう:
class Animal {
public:
virtual string GetType() const { return "Animal"; }
virtual string GetVoice() const { return "Voice"; }
};
class Dog : public Animal {
public:
string GetType() const override { return "Dog"; }
string GetVoice() const override { return "Woof"; }
};
class Cat : public Animal {
public:
string GetType() const override { return "Cat"; }
string GetVoice() const override { return "Miaow"; }
};
void Type(Animal& a) { cout<<a.GetType(); }
void Speak(Animal& a) { cout<<a.GetVoice(); }
int main()
{
Dog d; Type(d); cout<<" speak "; Speak(d); cout<<" - ";
Cat c; Type(c); cout<<" speak "; Speak(c); cout<<endl;
return 0;
}
ここでの正しい出力は:
"Dog speak Voice - Cat speak Voice"
Speak 関数の宣言は値渡しを使用しているため、オブジェクトスライシングが発生します。この場合、Speak 関数に渡される Animal オブジェクトは、Animal の基本クラス部分の情報のみが保持され、派生クラスの情報(Dog や Cat の特定の実装など)は失われます。
スライシングを回避する#
この問題は、Speak 関数をオブジェクト自体ではなく参照で受け取るように変更することで解決できます:
void Speak(Animal& a) { cout << a.GetVoice(); }
このように修正すると、ポリモーフィズムが正しく適用され、Speak (d) は "Woof" を出力し、Speak (c) は "Miaow" を出力します。
また、void Speak(Dog d)
やvoid Speak(Cat c)
などのオーバーロードを作成することもできます。そうすると、正しい関数が呼び出されます。
実際、最も一般的な方法は、参照またはポインタを使用してポリモーフィックな動作を保持することです。オーバーロードの方法は、コードの汎用性と拡張性を制限します。
仮想関数とポリモーフィズム#
仮想関数を使用しない場合、メソッド呼び出しは静的であり、つまりコンパイル時に呼び出す関数が決定されます。この場合、呼び出しの動作は宣言された型に依存し、実際の型には依存しません。
仮想関数がない場合、オブジェクトスライシングを回避していても(例えば参照やポインタを介して)、派生クラスのメソッドは基本クラスの参照やポインタを介して呼び出されません。基本クラスのポインタや参照を介して関数を呼び出すと、常に基本クラスのメソッドが呼び出され、派生クラスに存在する可能性のあるオーバーライドバージョンは呼び出されません。