派生クラスを値渡しすると情報が失われるオブジェクトスライシング
派生クラスのオブジェクトを基底クラス型の変数に値としてコピーすると、派生クラス独自の部分が切り捨てられ、仮想関数の呼び出しも基底クラスのものになってしまうバグです。
なぜエラーが出ないのか
出力: ...(Woofが呼ばれない)
(エラーなし)- C++は何も報告しません。コンパイルも実行も正常に完了するためです
出力: ...(Woofが呼ばれない)- 実際の挙動 — 期待した結果と食い違っている箇所
見つけ方- エラーが出ないので、出力を目で確かめるか、コードを目で追うしかありません。この種の誤りが最も発見が遅れます
このエラーが出る典型パターン
パターン1
1 #include <iostream> 2 class Animal { 3 public: 4 virtual std::string speak() { return "..."; } 5 }; 6 class Dog : public Animal { 7 public: 8 std::string speak() override { return "Woof"; } 9 }; 10 int main() { 11 Dog d; 12 Animal a = d; ^ 13 std::cout << a.speak() << std::endl; 14 return 0; 15 }
出力: ...(Woofが呼ばれない)期待: Woof
Animal a = d;は値としてコピーするため、Dog独自の部分が切り捨てられ、正真正銘のAnimalオブジェクトになります(オブジェクトスライシング)。参照(Animal&)で受け取ればスライシングを避けられます。
直し方: Animal を Animal& にします。
広告
広告スロット(未設定)
パターン2
1 #include <iostream> 2 class Vehicle { 3 public: 4 virtual std::string name() { return "vehicle"; } 5 }; 6 class Car : public Vehicle { 7 public: 8 std::string name() override { return "car"; } 9 }; 10 int main() { 11 Car c; 12 Vehicle v = c; ^ 13 std::cout << v.name() << std::endl; 14 return 0; 15 }
出力: vehicle(carが呼ばれない)期待: car
「基底クラス型の変数に代入しても、中身が派生クラスなら派生クラスとして振る舞う」というのは誤解です。値としてコピーした瞬間、vtable(仮想関数の呼び出し先情報)も失われます。
直し方: Vehicle を Vehicle& にします。
パターン3
1 #include <iostream> 2 class Employee { 3 public: 4 virtual std::string role() { return "employee"; } 5 }; 6 class Manager : public Employee { 7 public: 8 std::string role() override { return "manager"; } 9 }; 10 int main() { 11 Manager m; 12 Employee e = m; ^ 13 std::cout << e.role() << std::endl; 14 return 0; 15 }
出力: employee(managerが呼ばれない)期待: manager
ポインタや参照ではなく値でコピーする限り、代入先は常に基底クラスそのものになります。多態性を保つには参照かポインタで扱う必要があります。
直し方: Employee を Employee& にします。
よくある誤解
「基底クラス型の変数に代入しても、中身が派生クラスなら派生クラスとして振る舞う」というのは誤解です。ポインタや参照ではなく値としてコピーした瞬間、コピー先は正真正銘の基底クラスのオブジェクトになり、派生クラス部分のデータもvtable(仮想関数の呼び出し先情報)も失われます。
まとめ
派生クラスを値渡しすると情報が失われるオブジェクトスライシングは上級でつまずきやすい項目です。上の3パターンを実際に手で直すと、エラーメッセージのどこを読めばよいかが掴めます。