C++におけるオブジェクトスライシングとは?
オブジェクトスライシングが起こる理由
オブジェクトスライシングが起こるのは、C++におけるオブジェクトのメモリ配置と関係があります。C++では、オブジェクトはメモリ上に連続した領域に配置されます。基底クラスと派生クラスは、異なるメモリレイアウトを持つ可能性があります。
派生クラスのオブジェクトを基底クラスの変数に代入すると、基底クラスのメモリレイアウトに基づいてオブジェクトの一部のみがコピーされます。残りの部分は切り捨てられ、失われます。
オブジェクトスライシングの例
以下に、オブジェクトスライシングの例を示します。
class Animal {
public:
virtual void speak() = 0;
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
int main() {
Dog* dog = new Dog();
Animal* animal = dog; // オブジェクトスライシングが発生
// dog は "Woof!" と出力する
dog->speak();
// animal は "Woof!" と出力しない!基底クラスの speak() が呼び出される
animal->speak();
delete dog;
return 0;
}
この例では、Dog
クラスは Animal
クラスから派生しています。Dog
クラスには speak()
メソッドが追加されています。
main()
関数では、Dog
オブジェクト (dog
) を作成し、それを Animal
型のポインタ (animal
) に代入します。この代入によって、オブジェクトスライシングが発生します。
dog->speak()
を呼び出すと、Dog
クラスの speak()
メソッドが呼び出され、「Woof!」と出力されます。しかし、animal->speak()
を呼び出すと、Animal
クラスの speak()
メソッドが呼び出され、「Woof!」と出力されません。
これは、animal
ポインタが Dog
オブジェクトの一部しか指していないためです。animal
ポインタは、Animal
クラスのメンバー変数のみを指しており、Dog
クラスの追加メンバー変数は指していません。
オブジェクトスライシングを防ぐには、以下の方法があります。
- ポインタではなく、参照を使用する
オブジェクトスライシングは、ポインタを使用する場合にのみ発生します。参照を使用すれば、オブジェクト全体を参照することができ、オブジェクトスライシングを防ぐことができます。
Animal& animal = *dog; // オブジェクトスライシングを防ぐ
animal->speak(); // "Woof!" と出力される
- 基底クラスのポインタではなく、派生クラスのポインタを使用する
基底クラスのポインタではなく、派生クラスのポインタを使用すれば、派生クラス固有の情報も参照することができます。
Dog* animal = dog; // オブジェクトスライシングを防ぐ
animal->speak(); // "Woof!" と出力される
- 仮想関数を使用する
基底クラスで仮想関数を実装し、派生クラスでその仮想関数をオーバーライドすれば、オブジェクトスライシングの影響を受けずに派生クラス固有の機能を使用することができます。
#include <iostream>
class Animal {
public:
virtual void speak() = 0;
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!" << std::endl;
}
};
int main() {
// オブジェクトスライシングの例
// Dog オブジェクトを作成
Dog* dog = new Dog();
// Dog オブジェクトを Animal 型のポインタに代入
Animal* animal = dog;
// Animal 型のポインタ経由で speak() を呼び出す
animal->speak(); // "Woof!" と出力される
// オブジェクトスライシングを防ぐ例
// 参照を使用して Dog オブジェクトを参照
Animal& animal_ref = *dog;
// 参照経由で speak() を呼び出す
animal_ref.speak(); // "Woof!" と出力される
// 派生クラスのポインタを使用して Dog オブジェクトを参照
Dog* dog_ptr = dynamic_cast<Dog*>(animal);
// 派生クラスのポインタ経由で speak() を呼び出す
dog_ptr->speak(); // "Woof!" と出力される
// Cat オブジェクトを作成
Cat* cat = new Cat();
// Cat オブジェクトを Animal 型のポインタに代入
Animal* animal2 = cat;
// Animal 型のポインタ経由で speak() を呼び出す
animal2->speak(); // "Meow!" と出力される
delete dog;
delete cat;
return 0;
}
Woof!
Woof!
Woof!
Meow!
共有ポインタを使用する
std::shared_ptr
などの共有ポインタを使用すると、オブジェクトの所有権を複数のポインタで共有することができます。共有ポインタを使用すると、オブジェクトが削除されるまでメモリが解放されないため、オブジェクトスライシングを防ぐことができます。
std::shared_ptr<Dog> dog = std::make_shared<Dog>();
Animal* animal = dog.get();
// ...
dog.reset(); // オブジェクトが削除される
CRTP (Curiously Recurring Template Pattern) を使用する
CRTPは、テンプレートクラスを使用して、基底クラスと派生クラス間で情報を共有するパターンです。CRTPを使用すると、オブジェクトスライシングを防ぐことができます。
template <class T>
class CRTPBase {
protected:
T* self;
public:
CRTPBase() {
self = this;
}
};
class Animal : public CRTPBase<Animal> {
public:
virtual void speak() = 0;
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
int main() {
Dog* dog = new Dog();
Animal* animal = dog;
// ...
delete dog; // オブジェクトが正しく削除される
}
ユニークポインタを使用する
std::unique_ptr<Dog> dog = std::make_unique<Dog>();
Animal* animal = dog.get();
// ...
dog.reset(); // オブジェクトが削除される
c++ inheritance c++-faq