第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を使って、ログ機能を提供する基底クラスを実装してください。