第1章:サブタイプポリモーフィズム
はじめに
CPP Module 04では、C++のサブタイプポリモーフィズム(動的多相性)を学びます。これはオブジェクト指向プログラミングの核心的な機能であり、異なる型のオブジェクトを統一的に扱うことを可能にします。
---
1. ポリモーフィズムの概念
1.1 Christopher Stracheyの分類
1967年、Christopher Stracheyはポリモーフィズムを2種類に分類しました:
Ad-hoc polymorphism(アドホック多相性):
- オーバーロード:同名で異なるパラメータの関数
- 型強制(coercion):暗黙的型変換
Parametric polymorphism(パラメトリック多相性):
- テンプレート:型をパラメータ化
C++では、これに加えてSubtype polymorphism(サブタイプ多相性)があります:
- 継承と仮想関数による動的なメソッド解決
1.2 サブタイプポリモーフィズムとは
サブタイプポリモーフィズムは、基底クラスのポインタ/参照を通じて派生クラスのメソッドを呼び出すことを可能にします:
class Animal {
public:
virtual void makeSound() const {
std::cout << "Some sound" << std::endl;
}
};
class Dog : public Animal {
public:
void makeSound() const override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() const override {
std::cout << "Meow!" << std::endl;
}
};
void petSound(const Animal& animal) {
animal.makeSound(); // 実行時に適切なメソッドが呼ばれる
}
int main() {
Dog dog;
Cat cat;
petSound(dog); // "Woof!"
petSound(cat); // "Meow!"
return 0;
}
1.3 なぜ重要か
---
2. 仮想関数
2.1 virtualキーワード
virtualキーワードは、関数を動的にバインドすることを示します:
class Base {
public:
virtual void func() {
std::cout << "Base::func" << std::endl;
}
};
class Derived : public Base {
public:
void func() override { // overrideは省略可能だが推奨
std::cout << "Derived::func" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->func(); // "Derived::func" (動的バインディング)
delete ptr;
return 0;
}
2.2 virtualなしの場合
class Base {
public:
void func() { // virtualなし
std::cout << "Base::func" << std::endl;
}
};
class Derived : public Base {
public:
void func() {
std::cout << "Derived::func" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->func(); // "Base::func" (静的バインディング)
delete ptr;
return 0;
}
2.3 vtable(仮想関数テーブル)
仮想関数はvtable(仮想関数テーブル)を通じて実装されます:
クラス Derived のメモリレイアウト:
+-------------------+
| vptr | → vtable(仮想関数テーブルへのポインタ)
+-------------------+
| Base members |
+-------------------+
| Derived members|
+-------------------+
vtable for Derived:
+-------------------+
| &Derived::func | → Derivedの実装を指す
+-------------------+
| &Derived::other |
+-------------------+
2.4 仮想関数のコスト
ただし、現代のコンピュータではこのコストはほとんど無視できます。
---
3. 仮想デストラクタ
3.1 なぜ必要か
基底クラスポインタで派生クラスをdeleteする場合、仮想デストラクタがないとメモリリークが発生します:
class Base {
public:
~Base() { // 非仮想デストラクタ
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() {
std::cout << "Derived destructor" << std::endl;
delete[] data; // これが呼ばれない!
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // Base のデストラクタのみ呼ばれる
// メモリリーク!
return 0;
}
3.2 仮想デストラクタの使用
class Base {
public:
virtual ~Base() { // 仮想デストラクタ
std::cout << "Base destructor" << std::endl;
}
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() override {
std::cout << "Derived destructor" << std::endl;
delete[] data; // 正しく呼ばれる
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // Derivedのデストラクタ → Baseのデストラクタ
return 0;
}
3.3 ルール
> 基底クラスとして使用されるクラスは、常に仮想デストラクタを持つべき
---
4. 純粋仮想関数と抽象クラス
4.1 純粋仮想関数
純粋仮想関数は実装を持たず、派生クラスでの実装を強制します:
class Animal {
public:
virtual void makeSound() const = 0; // 純粋仮想関数
virtual ~Animal() {}
};
4.2 抽象クラス
1つ以上の純粋仮想関数を持つクラスは抽象クラスとなり、直接インスタンス化できません:
class Animal {
public:
virtual void makeSound() const = 0;
virtual ~Animal() {}
};
int main() {
// Animal animal; // エラー!抽象クラスはインスタンス化不可
Animal* ptr; // ポインタは可能
return 0;
}
4.3 抽象クラスの使用
class Animal {
public:
virtual void makeSound() const = 0;
virtual ~Animal() {}
};
class Dog : public Animal {
public:
void makeSound() const override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void makeSound() const override {
std::cout << "Meow!" << std::endl;
}
};
int main() {
Animal* animals[2];
animals[0] = new Dog();
animals[1] = new Cat();
for (int i = 0; i < 2; i++) {
animals[i]->makeSound();
delete animals[i];
}
return 0;
}
---
5. インターフェース
5.1 C++でのインターフェース
C++にはinterfaceキーワードはありませんが、純粋仮想関数のみを持つ抽象クラスでインターフェースを表現します:
class IAnimal { // 慣習的にIプレフィックス
public:
virtual void makeSound() const = 0;
virtual void move() const = 0;
virtual ~IAnimal() {}
};
5.2 インターフェースの実装
class IFlyable {
public:
virtual void fly() const = 0;
virtual ~IFlyable() {}
};
class ISwimmable {
public:
virtual void swim() const = 0;
virtual ~ISwimmable() {}
};
class Duck : public IFlyable, public ISwimmable {
public:
void fly() const override {
std::cout << "Duck flies" << std::endl;
}
void swim() const override {
std::cout << "Duck swims" << std::endl;
}
};
5.3 Javaとの比較
| 概念 | C++ | Java |
|------|-----|------|
| インターフェース | 純粋仮想関数のみの抽象クラス | interfaceキーワード |
| 抽象クラス | = 0で純粋仮想関数 | abstractキーワード |
| 多重継承 | クラス・インターフェース両方可能 | クラスは単一、インターフェースは複数 |
---
6. 深いコピー
6.1 浅いコピーの問題
ポインタメンバを持つクラスでは、デフォルトのコピーは浅いコピー(ポインタの値をコピー)になります:
class Brain {
public:
std::string ideas[100];
};
class Animal {
protected:
Brain* brain;
public:
Animal() : brain(new Brain()) {}
// デフォルトのコピーは浅いコピー
// Animal(const Animal& other) : brain(other.brain) {}
~Animal() { delete brain; }
};
int main() {
Animal a;
Animal b = a; // bはaと同じbrainを指す
// aとbの破棄時に同じbrainを2回delete → 未定義動作
return 0;
}
6.2 深いコピーの実装
class Brain {
public:
std::string ideas[100];
Brain() {}
Brain(const Brain& other) {
for (int i = 0; i < 100; i++) {
ideas[i] = other.ideas[i];
}
}
};
class Animal {
protected:
Brain* brain;
public:
Animal() : brain(new Brain()) {
std::cout << "Animal constructor" << std::endl;
}
Animal(const Animal& other) : brain(new Brain(*other.brain)) {
// 新しいBrainを作成し、内容をコピー
std::cout << "Animal copy constructor" << std::endl;
}
Animal& operator=(const Animal& other) {
std::cout << "Animal assignment operator" << std::endl;
if (this != &other) {
delete brain;
brain = new Brain(*other.brain);
}
return *this;
}
virtual ~Animal() {
std::cout << "Animal destructor" << std::endl;
delete brain;
}
};
---
7. 42課題での実装ポイント
7.1 ex00: Animal/Dog/Cat
class Animal {
protected:
std::string type;
public:
Animal();
Animal(const Animal& other);
Animal& operator=(const Animal& other);
virtual ~Animal();
virtual void makeSound() const;
std::string getType() const;
};
class Dog : public Animal {
public:
Dog();
Dog(const Dog& other);
Dog& operator=(const Dog& other);
~Dog();
void makeSound() const override;
};
class Cat : public Animal {
public:
Cat();
Cat(const Cat& other);
Cat& operator=(const Cat& other);
~Cat();
void makeSound() const override;
};
7.2 ex01: Brainクラスの追加
class Brain {
public:
std::string ideas[100];
Brain();
Brain(const Brain& other);
Brain& operator=(const Brain& other);
~Brain();
};
class Dog : public Animal {
private:
Brain* brain;
public:
Dog();
Dog(const Dog& other);
Dog& operator=(const Dog& other);
~Dog();
// ...
};
7.3 ex02: 抽象クラス化
class Animal {
protected:
std::string type;
public:
// ...
virtual void makeSound() const = 0; // 純粋仮想関数
};
// Animalは直接インスタンス化不可
7.4 ex03: インターフェース(Bonus)
class ICharacter {
public:
virtual ~ICharacter() {}
virtual std::string const& getName() const = 0;
virtual void equip(AMateria* m) = 0;
virtual void unequip(int idx) = 0;
virtual void use(int idx, ICharacter& target) = 0;
};
class IMateriaSource {
public:
virtual ~IMateriaSource() {}
virtual void learnMateria(AMateria*) = 0;
virtual AMateria* createMateria(std::string const& type) = 0;
};
---
8. デザインパターンとの関連
8.1 ファクトリパターン
class Animal {
public:
virtual ~Animal() {}
virtual void makeSound() const = 0;
static Animal* create(const std::string& type) {
if (type == "Dog") return new Dog();
if (type == "Cat") return new Cat();
return nullptr;
}
};
8.2 ストラテジーパターン
class SoundStrategy {
public:
virtual ~SoundStrategy() {}
virtual void execute() const = 0;
};
class BarkStrategy : public SoundStrategy {
public:
void execute() const override {
std::cout << "Woof!" << std::endl;
}
};
class Animal {
SoundStrategy* strategy;
public:
void setStrategy(SoundStrategy* s) { strategy = s; }
void makeSound() const { strategy->execute(); }
};
---
まとめ
本章で学んだこと:
- サブタイプポリモーフィズム: 動的なメソッド解決
- 仮想関数: virtualキーワードとvtable
- 仮想デストラクタ: メモリリーク防止
- 純粋仮想関数と抽象クラス:
= 0による強制 - インターフェース: 純粋仮想関数のみのクラス
- 深いコピー: ポインタメンバの正しいコピー
次章では、各演習の詳細な実装を解説します。