コンパイル時 vs 実行時ポリモーフィズム
CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 56/104。
ポリモーフィズム(Polymorphism)は「多くの形態」を意味し、オブジェクトを統一的に扱いながら異なる振る舞いをさせることを可能にする、オブジェクト指向プログラミング(OOP)の核となる概念です。C++は2つの異なるタイプのポリモーフィズムをサポートしており、それぞれがプログラム実行の異なる段階で解決されます。
コンパイル時ポリモーフィズム(静的ポリモーフィズムとも呼ばれます)は、プログラムが実行される前にコンパイラによって解決されます。コンパイラは、関数のシグネチャに基づいて、呼び出すべき関数を正確に決定します。これには、関数のオーバーロードやテンプレートが含まれます:
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // コンパイラは print(int) を選択します
print(3.14); // コンパイラは print(double) を選択します実行時ポリモーフィズム(動的ポリモーフィズムとも呼ばれます)は、プログラムの実行中に解決されます。どの関数を呼び出すかの決定は、ポインタや参照の型ではなく、実際のオブジェクトの型に依存します。これは仮想関数を通じて実現されます:
class Shape {
public:
virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing circle" << std::endl; }
};
Shape* s = new Circle();
s->draw(); // 実行時に決定されます: "Drawing circle"主なトレードオフ:コンパイル時ポリモーフィズムは、決定がコンパイル中に行われるため実行時のオーバーヘッドがゼロであるのに対し、実行時ポリモーフィズムはわずかなコスト(vtableルックアップ)が発生しますが、実行時まで型が特定できないオブジェクトを扱う際に大きな柔軟性を提供します。
チャレンジ
簡単2つのタイプのポリモーフィズムを並べて実証する計算機システムを構築しましょう。コンパイル時ポリモーフィズムが関数のオーバーロードを通じて異なる入力タイプを処理し、実行時ポリモーフィズムが異なる計算戦略を動的に切り替えることを可能にするシステムを作成します。
コードは3つのファイルに分けて構成します:
Calculator.h: あらゆる計算戦略を表すベースとなるCalculatorクラスを定義します:intを返し、"Base calculation: " <a> " ? " <b>と出力する仮想メソッドcalculate(int a, int b)(0を返します)- 仮想デストラクタ
Operations.h: 計算の振る舞いをオーバーライドする2つの派生計算機クラスを定義します:Adder:calculate()をオーバーライドして"Adding: " <a> " + " <b>と出力し、合計を返しますMultiplier:calculate()をオーバーライドして"Multiplying: " <a> " * " <b>と出力し、積を返します
overrideキーワードを使用してください。main.cpp: 両方のポリモーフィズムのタイプを示すシステムを作成します。2つの整数の入力(それぞれ別々の行)を読み取ります。まず、3つのオーバーロードされた
display()関数を作成して、コンパイル時ポリモーフィズムを実証します:display(int x)は"Integer value: " <x>と出力しますdisplay(double x)は"Double value: " <x>と出力しますdisplay(const std::string& x)は"String value: " <x>と出力します
次に、ベースの
Calculator、Adder、およびMultiplierを含むCalculator*ポインタの配列を作成して、実行時ポリモーフィズムを実証します。ループを回して、入力値を用いてそれぞれでcalculate()を呼び出し、各計算の後に結果を出力します。出力は次のように構成してください:
=== Compile-Time Polymorphism === <int, double, string の display 出力> === Runtime Polymorphism === <結果を含む calculate 出力>コンパイル時のセクションでは、最初の入力を整数として、次に double として(同じ値に .5 を加えたもの)、そして文字列 "Result" として
display()を呼び出します。終了したら、動的に割り当てた計算機をクリーンアップしてください。
例えば、入力が 10 と 3 の場合:
=== Compile-Time Polymorphism ===
Integer value: 10
Double value: 10.5
String value: Result
=== Runtime Polymorphism ===
Base calculation: 10 ? 3
Result: 0
Adding: 10 + 3
Result: 13
Multiplying: 10 * 3
Result: 30コンパイラが引数の型に基づいて正しい display() のオーバーロードを選択する(コンパイル時の決定)一方で、正しい calculate() メソッドは vtable メカニズムを通じて実行時の実際のオブジェクト型によって決定される様子に注目してください。
チートシート
C++は、異なる段階で解決される2種類のポリモーフィズムをサポートしています:
コンパイル時ポリモーフィズム(静的ポリモーフィズム)は、実行前にコンパイラによって解決されます。これには、関数のオーバーロードやテンプレートが含まれます:
void print(int x) { std::cout << "Integer: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
print(5); // コンパイラは print(int) を選択します
print(3.14); // コンパイラは print(double) を選択します実行時ポリモーフィズム(動的ポリモーフィズム)は、仮想関数を使用して実行中に解決されます。実際のオブジェクトの型によって、どの関数が呼び出されるかが決まります:
class Shape {
public:
virtual void draw() { std::cout << "Drawing shape" << std::endl; }
};
class Circle : public Shape {
public:
void draw() override { std::cout << "Drawing circle" << std::endl; }
};
Shape* s = new Circle();
s->draw(); // 実行時に決定されます: "Drawing circle"トレードオフ: コンパイル時ポリモーフィズムは実行時のオーバーヘッドがゼロですが、実行時ポリモーフィズムはわずかなコスト(vtableのルックアップ)が発生する代わりに、より高い柔軟性を提供します。
自分で試してみよう
#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"
// TODO: 3つのオーバーロードされた display() 関数を作成してください:
// 1. display(int x) - "Integer value: <x>" を出力
// 2. display(double x) - "Double value: <x>" を出力
// 3. display(const std::string& x) - "String value: <x>" を出力
int main() {
// 2つの整数入力を読み込む
int a, b;
std::cin >> a;
std::cin >> b;
// === コンパイル時ポリモーフィズム ===
std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
// TODO: 以下の引数で display() を呼び出してください:
// - 整数としての a
// - doubleとしての a (0.5 を加算する)
// - 文字列 "Result"
std::cout << std::endl;
// === 実行時ポリモーフィズム ===
std::cout << "=== Runtime Polymorphism ===" << std::endl;
// TODO: 3つの要素を持つ Calculator* ポインタの配列を作成してください:
// - 基底クラス Calculator
// - Adder
// - Multiplier
// TODO: 配列をループで回し、それぞれに対して calculate(a, b) を呼び出し、
// 各計算の後に "Result: <return_value>" を出力してください
// TODO: 動的に割り当てられたメモリをクリーンアップしてください
return 0;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
8ポリモーフィズム
コンパイル時 vs 実行時ポリモーフィズム関数オーバーロード仮想関数の再考純粋仮想関数抽象クラスC++におけるインターフェース設計Dynamic Casting & RTTIまとめ:図形計算機3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス