コンパイル時と実行時の多相性
CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 56/104。
ポリモーフィズムとは「多くの形態」を意味し、オブジェクトを一様に扱いながら異なる動作をさせることができる、OOPの中核的な概念です。C++は、プログラムの実行の異なる段階でそれぞれ解決される、2つの異なる種類のポリモーフィズムをサポートしています。
Compile-time ポリモーフィズム(静的ポリモーフィズムとも呼ばれます)は、プログラムの実行前にコンパイラによって解決されます。コンパイラは、関数シグネチャに基づいて、どの関数を呼び出すかを正確に決定します。これには、関数のオーバーロードとテンプレートが含まれます。
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種類のポリモーフィズムを並べて示す電卓システムを構築しましょう。function overloading によって compile-time polymorphism が異なる入力型を処理し、runtime polymorphism によって異なる calculation 戦略を dynamically 切り替えられるシステムを作成します。
コードを3つのファイルに分けて構成します。
Calculator.h:任意の calculation 戦略を表す baseCalculatorclass を定義します。int aとint bを受け取り、intを返して、Base calculation: <a> ? <b>を出力する virtualcalculate(int a, int b)method(0を返す)- virtual destructor
Operations.h:calculation の動作を override する2つの derived calculator class を定義します。Adder:calculate()を override してAdding: <a> + <b>を出力し、合計を返すMultiplier:calculate()を override してMultiplying: <a> * <b>を出力し、積を返す
overridekeyword を使用します。main.cpp:両方のポリモーフィズムの型を示すシステムを作成します。2つの integer inputs を読み込みます(それぞれ別の行に入力)。まず、3つの overloaded
display()functions を作成して、compile-time polymorphism を示します。display(int x)は次を出力します:Integer value: <x>display(double x)は次を出力します:Double value: <x>display(const std::string& x)は次を出力します:String value: <x>
次に、base
Calculator、Adder、Multiplierを格納するCalculator*pointers の array を作成して、runtime polymorphism を示します。各要素を Loop して、それぞれに入力値を使ってcalculate()を call し、各 calculation の後に結果を出力します。出力を次のように構成します。
=== Compile-Time Polymorphism === <display outputs for int, double, string> === Runtime Polymorphism === <calculate outputs with results>compile-time セクションでは、最初の入力を integer として、次に .5 を加えた同じ値を double として、最後に文字列 "Result" として
display()を call します。終了したら dynamically allocated calculators を Clean up してください。
例えば、入力が 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() overload を選択する(compile-time の decision)一方で、適切な calculate() method は、vtable mechanism を通じて runtime に実際の object type によって決定されることに注目してください。
自分で試してみよう
#include <iostream>
#include <string>
#include "Calculator.h"
#include "Operations.h"
// TODO: オーバーロードされた display() 関数を3つ作成する:
// 1. display(int x) - prints "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;
// === Compile-Time Polymorphism ===
std::cout << "=== Compile-Time Polymorphism ===" << std::endl;
// TODO: 次のように display() を呼び出す:
// - 整数としての a
// - double としての a(0.5 を加える)
// - 文字列 "Result"
std::cout << std::endl;
// === Runtime Polymorphism ===
std::cout << "=== Runtime Polymorphism ===" << std::endl;
// TODO: Calculator* ポインタの配列を3要素で作成する:
// - a base Calculator
// - Adder
// - Multiplier
// TODO: 配列をループし、それぞれで calculate(a, b) を呼び出し、
// そして各計算の後に "Result: <return_value>" を出力する
// TODO: 動的に割り当てられたメモリを解放する
return 0;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタパラメータ付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ徹底解説Rule of Three / Five / Zero復習 - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () の演算子オーバーロード型変換演算子復習 - Matrixクラス自分で練習してみよう: C++オンラインコンパイラ