ポリモーフィズムとインターフェース¶
一言で言うと¶
ポリモーフィズム(polymorphism、多態性)とは、同じ呼び出しコードが相手によって違う振る舞いをすること。インターフェースとは「何ができるか」だけを定めた約束で、ポリモーフィズムの受け口です。呼ぶ側は約束だけを知り、相手が誰かを知りません。
解決したい問題¶
「種類ごとに違う処理」をするコードは、素朴に書くと種類の列挙(switch)が呼び出し側に散らばります。種類を追加するたびに、すべてのswitchを探して直すことになります。
問題のある実装¶
// C++20(問題例)
enum class WeaponType { Sword, Bow, Staff };
void Attack(WeaponType type) {
switch (type) {
case WeaponType::Sword: /* 近接判定 */ break;
case WeaponType::Bow: /* 矢を生成 */ break;
case WeaponType::Staff: /* 魔法弾生成 */ break;
}
}
void PlayAttackAnim(WeaponType type) { switch (type) { /* また全種類… */ } }
void GetRange(WeaponType type) { /* また全種類… */ }
何が問題になるか¶
- 武器を1種類足すと、switchを書いた全関数を修正。1つでも漏れると実行時に発覚。
- 武器ごとのコードが機能別にバラバラの場所にあり、「弓の仕様」を知るには全switchを読む必要がある(低凝集)。
改善した実装¶
// C++20
#include <memory>
#include <vector>
class IWeapon { // インターフェース: 約束だけ
public:
virtual ~IWeapon() = default; // 基底経由でdeleteするための仮想デストラクタ
virtual void Attack() = 0;
virtual float Range() const = 0;
};
class Sword : public IWeapon { // 剣に関する決定がここに集まる(高凝集)
public:
void Attack() override { /* 近接判定 */ }
float Range() const override { return 1.5f; }
};
class Bow : public IWeapon {
public:
void Attack() override { /* 矢を生成 */ }
float Range() const override { return 20.0f; }
};
// 呼び出し側は相手が剣か弓かを知らない
void UseWeapon(IWeapon& weapon) {
if (/* 敵が */ weapon.Range() > 10.0f) { /* 遠距離の立ち回り */ }
weapon.Attack();
}
武器の追加 = 新クラスを1つ書くだけ。既存コードは無変更です(これを原則化したのが OCP)。
仕組み: 多態には種類がある¶
C++には「同じ呼び出しで違う振る舞い」を実現する仕組みが複数あり、いつ相手が決まるかが違います。
| 種類 | 仕組み | 相手が決まる時 | コスト |
|---|---|---|---|
| 実行時多態 | 仮想関数(上の例) | 実行時 | vtable経由の間接呼び出し(→ 仕組み) |
| コンパイル時多態 | テンプレート | コンパイル時 | 実行時ゼロ、コンパイル時間とコードサイズ増 |
| アドホック多態 | 関数オーバーロード | コンパイル時 | なし |
「実行中に武器を持ち替える」なら実行時多態、「固定の型ごとに最速のコードが欲しい」ならコンパイル時多態が向きます(→ 判断ガイド: テンプレートか仮想関数か)。
代わりに何を失ったか¶
- 一覧性: switch版は「全武器の攻撃処理」が1画面で読めた。クラス版は種類ごとにファイルが分かれ、全体を見渡しにくい。
- 性能: 仮想関数呼び出しはインライン化を阻害し、間接参照が入る(多くのゲームロジックでは誤差。ホットループでは効く → Data Locality)。
- 種類が増えない・2〜3種で固定なら、switchの方が単純で優れています。「switchは悪」ではなく「増え続けるswitchが悪」です。
インターフェース設計の要点¶
- 約束は呼ぶ側が必要とする最小限にする(太ったインターフェースは全実装を苦しめる → ISP)。
- C++にはC#の
interfaceキーワードがなく、「純粋仮想関数のみ+仮想デストラクタの抽象クラス」で表現するのが慣習です(→ 継承と仮想関数)。
ゲーム開発での例¶
- ダメージを受けられるものすべて(
IDamageable): 敵・箱・壁を同じ攻撃コードで処理。 - 入力デバイスの抽象(
IInputSource): パッド・キーボード・リプレイ再生を同じ操作コードで処理(→ ケーススタディ: プレイヤー入力)。
Unity/C# との対応¶
- C#の
interfaceがそのまま対応。GetComponent<IDamageable>()のようにインターフェースでコンポーネントを取れるのは強力な定石です。 - C#では全メソッドが既定で非virtualなのはC++と同じですが、C#のinterface実装は暗黙、C++のoverrideは明示という違いがあります(→ 第9部)。
Unreal Engine との対応¶
- UEのInterfaceは
UINTERFACEマクロで宣言する独自形式(Blueprint連携のため)。標準C++の抽象クラスとは書き方が異なります(→ 第10部)。
よくある誤解¶
- 「ポリモーフィズム = 継承」ではありません。テンプレートも
std::functionもポリモーフィズムの手段です。 - 「インターフェースは常に作るべき」でもありません。実装が1つしかなく増える見込みがないなら、具象クラス直接参照で十分です(→ 判断ガイド)。
使う場面 / 使わない場面¶
- 使う場面: 種類が増え続ける、実行時に差し替える、種類を知らないコード(汎用処理)を書きたい、テストでフェイクに差し替えたい。
- 使わない場面: 種類が固定で少ない、全種類を同時に見渡したい処理(バランス調整表のような)、性能が最優先のホットループ。
関連項目¶
理解度チェック¶
- ポリモーフィズムが解決する「switch散らばり問題」とは何ですか。
- 実行時多態とコンパイル時多態の違い(相手が決まる時期とコスト)を言えますか。
- switchのままにする方がよいのはどんなときですか。
演習¶
「拾えるもの(コイン・回復薬・武器)」を、(a) enum+switch、(b) IPickup インターフェース、の2通りで骨格実装してください。その後「拾うと3秒後に爆発する爆弾」を両方に追加し、変更したファイル数と行数を比較してください。
前: 継承・委譲・コンポジション | カテゴリ目次 | 次: 設計の品質特性