🐥

オブジェクトスライシング

2024/02/12に公開

はじめに

C++を書いていてつまずいたところの備忘録です。
誤りがあればご指摘いただけると嬉しいです。

つまずいたところ

C++で継承を利用したコードを書いていました。

派生クラスのインスタンスを基底クラスの値変数に代入して、オーバーライドしたメソッドを呼び出そうとします。
派生クラスでオーバーライドしたメソッドを呼び出したいのに、基底クラスのものが呼び出されました。
オーバーライドするメソッドは、基底クラスでvirtualキーワードをつけていました。

以下が実際のコードを簡略化したものです。

#include <iostream>

using namespace std;

class Base {
    public:
    virtual void hello() {
        cout << "Base: Hello" << endl;
    }
};

class Derived : public Base {
    public:
    void hello() {
        cout << "Derived: Hello" << endl;
    }
};

int main() {
    Base value_obj = Derived();
    value_obj.hello();
}

手順

DerivedクラスでBaseクラスのhelloメソッドをオーバーライドしたのち、
Baseクラスの値変数objDerivedクラスのインスタンスを格納しました。
オーバーライドしたhelloメソッドを呼び出しました。

結果

Derivedクラスでオーバーライドしたhelloメソッドは、Baseクラスでvirtualキーワードをつけていました。
しかし、Derivedクラスでなく、Baseクラスのメソッドが呼び出されました。

$ Base: Hello

原因

派生クラスのインスタンスを基底クラスの値変数に代入した際に、派生クラス固有の情報が切り落とされます。
このプロセス全体を指してオブジェクトスライシングといいます。

具体的には以下の情報が抜け落ちてしまいます。

  • 派生クラス固有のメンバ変数
  • 派生クラスでオーバーライドされた仮想関数
  • 派生クラスで新たに定義されたメンバ関数

派生クラスのインスタンスを基底クラスの値変数に代入すると、基底クラスの代入演算子が呼び出されます。
代入演算子はオブジェクトの各メンバをコピーするだけです。
そのため、派生クラスのメソッドやメンバ変数の情報が抜け落ちてしまうようです。

解決策

ポインタ、参照の基底クラス変数に代入することで解決できます。

class Base {
    // 省略
};

class Derived : public Base {
    // 省略
};

int main() {
    Base *ptr_obj = new Derived();
    ptr_obj->hello();
}

出力は以下の通りです。
$ Derived: Hello

他の解決策としては、代入演算子にvirtualキーワードをつけて、派生クラスでオーバーライドする方法もあるようです。

まとめ

基底クラスの値変数に派生クラスのインスタンスを代入すると、派生クラスの情報が抜け落ちるオブジェクトスライシングが発生します。
基底クラスをポインタ変数や参照変数で扱うことで回避できます。
今回は変数に格納した時の問題でしたが、関数の引数を値渡しにしていると同様の問題に遭遇するようです。
その場合も同様に、関数の引数を参照渡しにすることで回避できます。

参考

c++ - What is object slicing? - Stack Overflow

GitHubで編集を提案

Discussion