Code Fix

上級

派生クラスを値渡しすると情報が失われるオブジェクトスライシング

派生クラスのオブジェクトを基底クラス型の変数に値としてコピーすると、派生クラス独自の部分が切り捨てられ、仮想関数の呼び出しも基底クラスのものになってしまうバグです。

なぜエラーが出ないのか

出力: ...(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& にします。

この問題を解いてみる →

パターン4

 1  #include <iostream>
 2  class Bird {
 3  public:
 4      virtual std::string sound() { return "..."; }
 5  };
 6  class Duck : public Bird {
 7  public:
 8      std::string sound() override { return "Quack"; }
 9  };
10  int main() {
11      Duck d;
12      Bird  b = d;
          ^
13      std::cout << b.sound() << std::endl;
14      return 0;
15  }
出力: ...(Quackが呼ばれない)期待: Quack

値としてコピーすると、Duck独自の部分が切り捨てられ正真正銘のBirdになります。

直し方: Bird を Bird& にします。

この問題を解いてみる →

パターン5

 1  #include <iostream>
 2  class Instrument {
 3  public:
 4      virtual std::string play() { return "..."; }
 5  };
 6  class Piano : public Instrument {
 7  public:
 8      std::string play() override { return "Ding"; }
 9  };
10  int main() {
11      Piano p;
12      Instrument  i = p;
             ^
13      std::cout << i.play() << std::endl;
14      return 0;
15  }
出力: ...(Dingが呼ばれない)期待: Ding

参照で受け取ればスライシングを避けられ、多態性が保たれます。

直し方: Instrument を Instrument& にします。

この問題を解いてみる →

よくある誤解

「基底クラス型の変数に代入しても、中身が派生クラスなら派生クラスとして振る舞う」というのは誤解です。ポインタや参照ではなく値としてコピーした瞬間、コピー先は正真正銘の基底クラスのオブジェクトになり、派生クラス部分のデータもvtable(仮想関数の呼び出し先情報)も失われます。

実務での勘所

この問題を防ぐ唯一確実な方法は、多態的に扱いたいオブジェクトを値ではなく、常にポインタか参照で持ち回ることです。関数の引数や戻り値、コンテナへの格納方法として単純にBase(値)を選んでしまうと、その時点でスライシングの危険が生まれます。多態性(実行時に実際の型に応じた振る舞いをすること)を活かしたい設計では、Base&やBase*、あるいはstd::unique_ptr<Base>のような形で扱うのが鉄則です。値渡し・値格納は、そもそも多態性を必要としない単純なデータにだけ使うべきです。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)