第7章:ポリモーフィズム

はじめに

ポリモーフィズム(多態性)は、オブジェクト指向プログラミングの三大要素(カプセル化、継承、ポリモーフィズム)の一つです。Christopher Stracheyは1967年の講義ノート「Fundamental Concepts in Programming Languages」で、ポリモーフィズムを体系的に分類しました。

本章では、ポリモーフィズムの理論的背景から、C++での実践的な実装まで詳しく学びます。

---

1. ポリモーフィズムの分類

1.1 Stracheyの分類(1967年)

Christopher Stracheyは、ポリモーフィズムを2つの大きなカテゴリに分類しました:

ポリモーフィズム
├── アドホック・ポリモーフィズム(Ad-hoc Polymorphism)
│   ├── オーバーロード(Overloading)
│   └── 型強制(Coercion)
│
└── パラメトリック・ポリモーフィズム(Parametric Polymorphism)
    └── ジェネリクス/テンプレート

後にCardelli & Wegner(1985年)はこれを拡張:

ポリモーフィズム
├── ユニバーサル(Universal)
│   ├── パラメトリック(Parametric)- テンプレート
│   └── インクルージョン(Inclusion)- 継承/サブタイピング
│
└── アドホック(Ad-hoc)
    ├── オーバーロード(Overloading)
    └── 型強制(Coercion)

1.2 C++における分類

C++では、以下のように実現されます:

| 分類 | C++での実現 | バインディング時期 | |------|-------------|-------------------| | オーバーロード | 関数オーバーロード | コンパイル時(静的) | | 型強制 | 暗黙の型変換 | コンパイル時 | | パラメトリック | テンプレート | コンパイル時 | | インクルージョン | 仮想関数 | 実行時(動的) |

---

2. 静的ポリモーフィズム

2.1 関数オーバーロード

同じ名前で異なるシグネチャを持つ関数を定義:

class Calculator {
public:
    // 整数の加算
    int add(int a, int b) {
        return a + b;
    }

    // 浮動小数点の加算
    double add(double a, double b) {
        return a + b;
    }

    // 3つの整数の加算
    int add(int a, int b, int c) {
        return a + b + c;
    }

    // 文字列の連結
    std::string add(const std::string& a, const std::string& b) {
        return a + b;
    }
};

int main() {
    Calculator calc;
    std::cout << calc.add(1, 2) << std::endl;        // 3
    std::cout << calc.add(1.5, 2.5) << std::endl;    // 4.0
    std::cout << calc.add(1, 2, 3) << std::endl;     // 6
    std::cout << calc.add("Hello", " World") << std::endl;  // Hello World
    return 0;
}

2.2 オーバーロードの解決規則

コンパイラは以下の優先順位でオーバーロードを解決します:

  • 完全一致(exact match)
  • 昇格(promotion): char → int, float → double
  • 標準変換(standard conversion): int → double
  • ユーザー定義変換(user-defined conversion)

void func(int x) { std::cout << "int" << std::endl; }
void func(double x) { std::cout << "double" << std::endl; }
void func(char x) { std::cout << "char" << std::endl; }

int main() {
    func(42);       // int(完全一致)
    func(3.14);     // double(完全一致)
    func('a');      // char(完全一致)
    func(42.0f);    // double(昇格: float → double)
    func(42L);      // 曖昧になる可能性(long → int/double)
    return 0;
}

2.3 演算子オーバーロード

演算子もオーバーロードの一種です:

class Complex {
    double real, imag;

public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    // 加算演算子のオーバーロード
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }

    // 単項マイナス
    Complex operator-() const {
        return Complex(-real, -imag);
    }

    // 出力演算子(フレンド関数)
    friend std::ostream& operator<<(std::ostream& os, const Complex& c);
};

std::ostream& operator<<(std::ostream& os, const Complex& c) {
    os << c.real << " + " << c.imag << "i";
    return os;
}

2.4 テンプレートによる静的ポリモーフィズム

テンプレートはコンパイル時に具体的な型で展開されます:

template <typename T>
T maximum(T a, T b) {
    return (a > b) ? a : b;
}

int main() {
    std::cout << maximum(3, 5) << std::endl;       // int版が生成
    std::cout << maximum(3.14, 2.71) << std::endl; // double版が生成
    std::cout << maximum('a', 'z') << std::endl;   // char版が生成
    return 0;
}

コンパイル時に生成されるコード

// コンパイラが以下を生成
int maximum(int a, int b) { return (a > b) ? a : b; }
double maximum(double a, double b) { return (a > b) ? a : b; }
char maximum(char a, char b) { return (a > b) ? a : b; }

---

3. 動的ポリモーフィズム

3.1 仮想関数による多態性

継承と仮想関数を使った実行時ポリモーフィズム:

class Shape {
public:
    virtual double area() const = 0;  // 純粋仮想関数
    virtual void draw() const = 0;
    virtual ~Shape() {}
};

class Circle : public Shape {
    double radius;
public:
    Circle(double r) : radius(r) {}

    double area() const override {
        return 3.14159 * radius * radius;
    }

    void draw() const override {
        std::cout << "Drawing circle with radius " << radius << std::endl;
    }
};

class Rectangle : public Shape {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double area() const override {
        return width * height;
    }

    void draw() const override {
        std::cout << "Drawing rectangle " << width << "x" << height << std::endl;
    }
};

void processShapes(const std::vector<Shape*>& shapes) {
    for (const Shape* shape : shapes) {
        shape->draw();                           // 動的バインディング
        std::cout << "Area: " << shape->area() << std::endl;
    }
}

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle(5.0));
    shapes.push_back(new Rectangle(4.0, 3.0));
    shapes.push_back(new Circle(2.0));

    processShapes(shapes);

    for (Shape* shape : shapes) {
        delete shape;
    }
    return 0;
}

3.2 vtable(仮想関数テーブル)の仕組み

仮想関数はvtableを通じて実現されます:

Circle オブジェクトのメモリレイアウト:
+------------------+
| vptr             | ──→ Circle vtable
+------------------+      +------------------+
| radius           |      | &Circle::area    |
+------------------+      +------------------+
                          | &Circle::draw    |
                          +------------------+
                          | &Circle::~Circle |
                          +------------------+

Rectangle オブジェクトのメモリレイアウト:
+------------------+
| vptr             | ──→ Rectangle vtable
+------------------+      +---------------------+
| width            |      | &Rectangle::area    |
+------------------+      +---------------------+
| height           |      | &Rectangle::draw    |
+------------------+      +---------------------+
                          | &Rectangle::~Rect...|
                          +---------------------+

仮想関数呼び出しの流れ

  • オブジェクトのvptrを取得
  • vtableから関数アドレスを取得
  • 関数を呼び出し

// shape->area() の内部動作(疑似コード)
vptr = shape->vptr;
area_func = vptr[0];  // vtableの0番目
(*area_func)(shape);  // 関数呼び出し

3.3 純粋仮想関数と抽象クラス

純粋仮想関数を持つクラスは抽象クラスとなり、インスタンス化できません:

class AbstractBase {
public:
    virtual void pureVirtual() = 0;  // 純粋仮想関数
    virtual void normalVirtual() {   // 通常の仮想関数
        std::cout << "Default implementation" << std::endl;
    }
    virtual ~AbstractBase() {}
};

// AbstractBase base;  // エラー:抽象クラスはインスタンス化できない

class Concrete : public AbstractBase {
public:
    void pureVirtual() override {
        std::cout << "Concrete implementation" << std::endl;
    }
};

Concrete c;  // OK:全ての純粋仮想関数を実装している

3.4 インターフェースとしての抽象クラス

純粋仮想関数のみを持つクラスは、Javaのインターフェースに相当:

// インターフェース的な使い方
class IDrawable {
public:
    virtual void draw() const = 0;
    virtual ~IDrawable() {}
};

class IPrintable {
public:
    virtual void print() const = 0;
    virtual ~IPrintable() {}
};

// 複数のインターフェースを実装
class Document : public IDrawable, public IPrintable {
public:
    void draw() const override {
        std::cout << "Drawing document" << std::endl;
    }

    void print() const override {
        std::cout << "Printing document" << std::endl;
    }
};

---

4. Liskov置換原則(LSP)

4.1 定義

Barbara LiskovとJeannette Wingは1994年の論文「A Behavioral Notion of Subtyping」で、以下の原則を定式化しました:

> If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program. > > (SがTのサブタイプであるならば、プログラムの望ましい特性を変更することなく、型Tのオブジェクトを型Sのオブジェクトで置換できなければならない)

4.2 LSP違反の例

// 古典的な例:正方形は長方形か?

class Rectangle {
protected:
    int width, height;

public:
    virtual void setWidth(int w) { width = w; }
    virtual void setHeight(int h) { height = h; }

    int getWidth() const { return width; }
    int getHeight() const { return height; }
    int area() const { return width * height; }
};

class Square : public Rectangle {
public:
    // 正方形は幅と高さが同じ必要がある
    void setWidth(int w) override {
        width = w;
        height = w;  // 高さも変更!
    }

    void setHeight(int h) override {
        width = h;   // 幅も変更!
        height = h;
    }
};

void testRectangle(Rectangle& r) {
    r.setWidth(5);
    r.setHeight(4);
    // 期待: 面積 = 5 * 4 = 20
    assert(r.area() == 20);  // Squareでは失敗!
}

int main() {
    Rectangle rect;
    testRectangle(rect);  // OK

    Square sq;
    testRectangle(sq);    // 失敗!面積は16になる
    return 0;
}

4.3 LSPを守る設計

// 解決策:共通の抽象基底クラスを使う

class Shape {
public:
    virtual int area() const = 0;
    virtual ~Shape() {}
};

class Rectangle : public Shape {
    int width, height;
public:
    Rectangle(int w, int h) : width(w), height(h) {}
    int area() const override { return width * height; }
};

class Square : public Shape {
    int side;
public:
    Square(int s) : side(s) {}
    int area() const override { return side * side; }
};

// RectangleとSquareは別の型として扱う

4.4 LSPの契約

派生クラスは以下を守る必要があります:

  • 事前条件を強化しない: 基底クラスが受け入れる入力は、派生クラスも受け入れる
  • 事後条件を弱化しない: 派生クラスの出力は、基底クラスの約束を守る
  • 不変条件を維持する: オブジェクトの不変条件は維持される

class Base {
public:
    // 事前条件: x > 0
    // 事後条件: 戻り値 > 0
    virtual int process(int x) {
        assert(x > 0);
        int result = x * 2;
        assert(result > 0);
        return result;
    }
};

class Derived : public Base {
public:
    int process(int x) override {
        // OK: 事前条件を緩和(x >= 0も受け入れる)
        // NG: 事前条件を強化してはいけない(x > 10など)

        int result = x * 2;

        // 事後条件は維持する必要がある
        assert(result >= 0);  // 基底クラスの約束を守る
        return result;
    }
};

---

5. RTTI(実行時型情報)

5.1 dynamic_cast

実行時に安全な型変換を行います:

class Animal {
public:
    virtual ~Animal() {}
    virtual void speak() = 0;
};

class Dog : public Animal {
public:
    void speak() override { std::cout << "Woof!" << std::endl; }
    void fetch() { std::cout << "Fetching..." << std::endl; }
};

class Cat : public Animal {
public:
    void speak() override { std::cout << "Meow!" << std::endl; }
    void purr() { std::cout << "Purring..." << std::endl; }
};

void handleAnimal(Animal* animal) {
    animal->speak();  // 動的バインディング

    // 特定の型にキャストを試みる
    if (Dog* dog = dynamic_cast<Dog*>(animal)) {
        dog->fetch();  // Dog固有のメソッド
    }
    else if (Cat* cat = dynamic_cast<Cat*>(animal)) {
        cat->purr();   // Cat固有のメソッド
    }
}

int main() {
    Dog dog;
    Cat cat;

    handleAnimal(&dog);  // Woof! Fetching...
    handleAnimal(&cat);  // Meow! Purring...

    return 0;
}

5.2 参照へのdynamic_cast

参照に対してdynamic_castを使う場合、失敗すると例外が投げられます:

void processAnimal(Animal& animal) {
    try {
        Dog& dog = dynamic_cast<Dog&>(animal);
        dog.fetch();
    } catch (const std::bad_cast& e) {
        std::cout << "Not a dog: " << e.what() << std::endl;
    }
}

5.3 typeid演算子

オブジェクトの型情報を取得:

#include <typeinfo>

void checkType(Animal& animal) {
    std::cout << "Type: " << typeid(animal).name() << std::endl;

    if (typeid(animal) == typeid(Dog)) {
        std::cout << "This is a Dog" << std::endl;
    }
}

int main() {
    Dog dog;
    Cat cat;

    checkType(dog);  // Type: Dog (実装依存)
    checkType(cat);  // Type: Cat (実装依存)

    return 0;
}

5.4 RTTIのオーバーヘッドと代替手段

RTTIにはオーバーヘッドがあるため、以下の代替手段も検討:

1. 仮想関数パターン

class Animal {
public:
    virtual bool isDog() const { return false; }
    virtual bool isCat() const { return false; }
};

class Dog : public Animal {
public:
    bool isDog() const override { return true; }
};

2. 列挙型タグ

enum class AnimalType { Dog, Cat, Bird };

class Animal {
public:
    virtual AnimalType getType() const = 0;
};

class Dog : public Animal {
public:
    AnimalType getType() const override { return AnimalType::Dog; }
};

3. Visitorパターン(後述)

---

6. デザインパターンにおけるポリモーフィズム

6.1 Strategy パターン

アルゴリズムを動的に切り替え可能にする:

// 戦略インターフェース
class SortStrategy {
public:
    virtual void sort(std::vector<int>& data) = 0;
    virtual ~SortStrategy() {}
};

// 具体的な戦略
class BubbleSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override {
        std::cout << "Bubble sorting..." << std::endl;
        // バブルソート実装
    }
};

class QuickSort : public SortStrategy {
public:
    void sort(std::vector<int>& data) override {
        std::cout << "Quick sorting..." << std::endl;
        // クイックソート実装
    }
};

// コンテキスト
class Sorter {
    SortStrategy* strategy;
public:
    Sorter(SortStrategy* s) : strategy(s) {}

    void setStrategy(SortStrategy* s) { strategy = s; }

    void performSort(std::vector<int>& data) {
        strategy->sort(data);
    }
};

int main() {
    std::vector<int> data = {5, 2, 8, 1, 9};

    BubbleSort bubble;
    QuickSort quick;

    Sorter sorter(&bubble);
    sorter.performSort(data);  // Bubble sorting...

    sorter.setStrategy(&quick);
    sorter.performSort(data);  // Quick sorting...

    return 0;
}

6.2 Factory パターン

オブジェクトの生成をサブクラスに委譲:

class Product {
public:
    virtual void use() = 0;
    virtual ~Product() {}
};

class ConcreteProductA : public Product {
public:
    void use() override { std::cout << "Using Product A" << std::endl; }
};

class ConcreteProductB : public Product {
public:
    void use() override { std::cout << "Using Product B" << std::endl; }
};

class Factory {
public:
    virtual Product* createProduct() = 0;
    virtual ~Factory() {}
};

class FactoryA : public Factory {
public:
    Product* createProduct() override {
        return new ConcreteProductA();
    }
};

class FactoryB : public Factory {
public:
    Product* createProduct() override {
        return new ConcreteProductB();
    }
};

void clientCode(Factory& factory) {
    Product* product = factory.createProduct();
    product->use();
    delete product;
}

6.3 Visitor パターン

オブジェクト構造を変更せずに新しい操作を追加:

// 前方宣言
class Circle;
class Rectangle;

// Visitor インターフェース
class ShapeVisitor {
public:
    virtual void visit(Circle& circle) = 0;
    virtual void visit(Rectangle& rect) = 0;
    virtual ~ShapeVisitor() {}
};

// 要素インターフェース
class Shape {
public:
    virtual void accept(ShapeVisitor& visitor) = 0;
    virtual ~Shape() {}
};

class Circle : public Shape {
public:
    double radius;
    Circle(double r) : radius(r) {}

    void accept(ShapeVisitor& visitor) override {
        visitor.visit(*this);
    }
};

class Rectangle : public Shape {
public:
    double width, height;
    Rectangle(double w, double h) : width(w), height(h) {}

    void accept(ShapeVisitor& visitor) override {
        visitor.visit(*this);
    }
};

// 具体的なVisitor: 面積計算
class AreaCalculator : public ShapeVisitor {
public:
    double totalArea = 0;

    void visit(Circle& circle) override {
        totalArea += 3.14159 * circle.radius * circle.radius;
    }

    void visit(Rectangle& rect) override {
        totalArea += rect.width * rect.height;
    }
};

// 具体的なVisitor: 描画
class DrawVisitor : public ShapeVisitor {
public:
    void visit(Circle& circle) override {
        std::cout << "Drawing circle with radius " << circle.radius << std::endl;
    }

    void visit(Rectangle& rect) override {
        std::cout << "Drawing rectangle " << rect.width << "x" << rect.height << std::endl;
    }
};

int main() {
    std::vector<Shape*> shapes;
    shapes.push_back(new Circle(5.0));
    shapes.push_back(new Rectangle(4.0, 3.0));

    AreaCalculator areaCalc;
    DrawVisitor drawer;

    for (Shape* shape : shapes) {
        shape->accept(areaCalc);
        shape->accept(drawer);
    }

    std::cout << "Total area: " << areaCalc.totalArea << std::endl;

    for (Shape* shape : shapes) {
        delete shape;
    }
    return 0;
}

---

7. 静的 vs 動的ポリモーフィズムの選択

7.1 比較表

| 観点 | 静的(テンプレート) | 動的(仮想関数) | |------|---------------------|------------------| | バインディング | コンパイル時 | 実行時 | | パフォーマンス | 高速(インライン化可能) | オーバーヘッドあり | | 柔軟性 | 型がコンパイル時に決定 | 実行時に型を決定可能 | | バイナリサイズ | 増加(型ごとにコード生成) | 小さい | | コンパイル時間 | 長い | 短い | | エラーメッセージ | 複雑 | 分かりやすい |

7.2 使い分けの指針

静的ポリモーフィズムを選ぶ場合

  • パフォーマンスが最優先
  • 型がコンパイル時に決定できる
  • 汎用アルゴリズム(STLなど)

動的ポリモーフィズムを選ぶ場合

  • 実行時に型を決定する必要がある
  • プラグインシステム
  • オブジェクトのコレクションを統一的に扱う

7.3 CRTPによる静的ポリモーフィズム

Curiously Recurring Template Pattern:

template <typename Derived>
class Shape {
public:
    double area() const {
        return static_cast<const Derived*>(this)->areaImpl();
    }
};

class Circle : public Shape<Circle> {
    double radius;
public:
    Circle(double r) : radius(r) {}

    double areaImpl() const {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape<Rectangle> {
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}

    double areaImpl() const {
        return width * height;
    }
};

template <typename T>
void printArea(const Shape<T>& shape) {
    std::cout << "Area: " << shape.area() << std::endl;
}

int main() {
    Circle c(5.0);
    Rectangle r(4.0, 3.0);

    printArea(c);  // Area: 78.5397
    printArea(r);  // Area: 12

    return 0;
}

CRTPの利点:

  • 仮想関数のオーバーヘッドなし
  • コンパイル時に最適化可能
  • インライン化が可能
  • ---

    まとめ

    本章で学んだこと:

  • ポリモーフィズムの分類: Stracheyの分類、Cardelli & Wegnerの拡張
  • 静的ポリモーフィズム: オーバーロード、テンプレート
  • 動的ポリモーフィズム: 仮想関数、vtable
  • Liskov置換原則: サブタイプの正しい設計
  • RTTI: dynamic_cast、typeid
  • デザインパターン: Strategy、Factory、Visitor
  • 選択指針: 静的 vs 動的の使い分け
  • 次章では、テンプレートについてさらに深く学びます。

    ---

    練習問題

  • 以下のクラス階層でLSP違反があるか確認し、修正してください:
class Bird {
public:
    virtual void fly() { std::cout << "Flying" << std::endl; }
};

class Penguin : public Bird {
public:
    void fly() override {
        throw std::runtime_error("Penguins can't fly!");
    }
};

  • Strategyパターンを使って、異なる圧縮アルゴリズム(ZIP、GZIP、LZ4)を切り替え可能なクラスを設計してください。
  • 以下のコードの出力を予測してください:
class Base {
public:
    virtual void func() { std::cout << "Base" << std::endl; }
};

class Derived : public Base {
public:
    void func() override { std::cout << "Derived" << std::endl; }
};

int main() {
    Base* ptr = new Derived();
    ptr->func();

    Base& ref = *ptr;
    ref.func();

    Base obj = *ptr;  // スライシング
    obj.func();

    delete ptr;
    return 0;
}

  • CRTPを使って、ログ機能を提供する基底クラスを実装してください。