動的ポリモーフィズムと静的ポリモーフィズムの詳細

前回の投稿「動的および静的ポリモーフィズム」で、動的ポリモーフィズムを紹介しました。今日は、静的ポリモーフィズムを続けて、C++ の非常に興味深いイディオムを紹介します:不思議な繰り返しテンプレート パターン (CRTP)。

短い要約。これは前回の投稿で残した場所です。

動的ポリモーフィズムはオブジェクト指向に基づいており、インターフェイスとクラス階層の実装を分離することができます。レイト ダイナミック ディスパッチを取得するには、仮想性と、ポインターや参照などの間接性という 2 つの要素が必要です。次のプログラムは、動的ポリモーフィズムの例です:

// dispatchDynamicPolymorphism.cpp

#include <chrono> #include <iostream> auto start = std::chrono::steady_clock::now(); void writeElapsedTime(){ auto now = std::chrono::steady_clock::now(); std::chrono::duration<double> diff = now - start; std::cerr << diff.count() << " sec. elapsed: "; } struct MessageSeverity{ virtual void writeMessage() const { std::cerr << "unexpected" << '\n'; } }; struct MessageInformation: MessageSeverity{ void writeMessage() const override { std::cerr << "information" << '\n'; } }; struct MessageWarning: MessageSeverity{ void writeMessage() const override { std::cerr << "warning" << '\n'; } }; struct MessageFatal: MessageSeverity{}; void writeMessageReference(const MessageSeverity& messServer){ // (1) writeElapsedTime(); messServer.writeMessage(); } void writeMessagePointer(const MessageSeverity* messServer){ // (2) writeElapsedTime(); messServer->writeMessage(); } int main(){ std::cout << '\n'; MessageInformation messInfo; MessageWarning messWarn; MessageFatal messFatal; MessageSeverity& messRef1 = messInfo; MessageSeverity& messRef2 = messWarn; MessageSeverity& messRef3 = messFatal; writeMessageReference(messRef1); writeMessageReference(messRef2); writeMessageReference(messRef3); std::cerr << '\n'; MessageSeverity* messPoin1 = new MessageInformation; MessageSeverity* messPoin2 = new MessageWarning; MessageSeverity* messPoin3 = new MessageFatal; writeMessagePointer(messPoin1); writeMessagePointer(messPoin2); writeMessagePointer(messPoin3); std::cout << '\n'; }

静的ポリモーフィズムはテンプレートに基づいています。 C を使用してプログラムをリファクタリングしましょう 妙にR ecurringT テンプレート P アターン (CRTP)。

静的ポリモーフィズム

前のプログラムをリファクタリングする前に dispatchDynamicPolymorphism.cpp, これが CRTP の重要なアイデアです:クラス Derived クラス テンプレート Base から派生します と Base Derived あります テンプレート引数として。

template <typename T>
class Base
{
 ...
};

class Derived : public Base<Derived>
{
 ...
};

CRTP の純粋な性質は次のとおりです。

// crtp.cpp

#include <iostream>

template <typename Derived>
struct Base{
 void interface(){  // (2)
 static_cast<Derived*>(this)->implementation();
 }
 void implementation(){  // (3)
 std::cout << "Implementation Base" << std::endl;
 }
};

struct Derived1: Base<Derived1>{
 void implementation(){
 std::cout << "Implementation Derived1" << std::endl;
 }
};

struct Derived2: Base<Derived2>{
 void implementation(){
 std::cout << "Implementation Derived2" << std::endl;
 }
};

struct Derived3: Base<Derived3>{}; // (4)

template <typename T> // (1)
void execute(T& base){
 base.interface();
}


int main(){
 
 std::cout << '\n';
 
 Derived1 d1;
 execute(d1);
 
 Derived2 d2;
 execute(d2);
 
 Derived3 d3;
 execute(d3);
 
 std::cout << '\n';
 
}

関数テンプレート execute で使用します (1 行目) 静的ポリモーフィズム。各ベースはメソッド base.interfaceを呼び出しました .メンバー関数 Base::interface (2 行目) は、CRTP イディオムの要点です。メンバー関数は、派生クラスの実装にディスパッチします :static_cast(this)->implementation()。メソッドが呼び出されたときにインスタンス化されるため、これが可能です。この時点で派生クラス Derived1, Derived2 、および Derived3 は完全に定義されています。したがって、メソッド Base::interface はその派生クラスの実装を使用できます。非常に興味深いのは、メンバ関数 Base::implementation (3 行目) です。この関数は、クラス Derived3 の静的ポリモーフィズムのデフォルト実装の役割を果たします (4行目).

プログラムの出力は次のとおりです。

では、次のステップに進み、プログラム dispatchDynamicPolymorphism.cpp. をリファクタリングしましょう。

// dispatchStaticPolymorphism.cpp

#include <chrono>
#include <iostream>

auto start = std::chrono::steady_clock::now();

void writeElapsedTime(){
 auto now = std::chrono::steady_clock::now();
 std::chrono::duration<double> diff = now - start;
 
 std::cerr << diff.count() << " sec. elapsed: ";
}

template <typename ConcreteMessage> // (1)
struct MessageSeverity{
 void writeMessage(){ // (2)
 static_cast<ConcreteMessage*>(this)->writeMessageImplementation();
 }
 void writeMessageImplementation() const {
 std::cerr << "unexpected" << std::endl;
 }
};

struct MessageInformation: MessageSeverity<MessageInformation>{
 void writeMessageImplementation() const { // (3)
 std::cerr << "information" << std::endl;
 }
};

struct MessageWarning: MessageSeverity<MessageWarning>{
 void writeMessageImplementation() const { // (4)
 std::cerr << "warning" << std::endl;
 }
};

struct MessageFatal: MessageSeverity<MessageFatal>{}; // (5)

template <typename T>
void writeMessage(T& messServer){ 
 
 writeElapsedTime(); 
 messServer.writeMessage(); // (6)
 
}

int main(){

 std::cout << std::endl;
 
 MessageInformation messInfo;
 writeMessage(messInfo);
 
 MessageWarning messWarn;
 writeMessage(messWarn);
 
 MessageFatal messFatal;
 writeMessage(messFatal);
 
 std::cout << std::endl;

}

この場合、すべての具象クラス (3、4、および 5 行目) は基本クラス MessageSeverity から派生します。 .メンバ関数 writeMessage 具体的な実装 writeMessageImplementation にディスパッチするインターフェースです .これを実現するために、オブジェクトは ConcreteMessage:  static_cast<ConcreteMessage*>(this)->writeMessageImplementation(); にアップキャストされます .これはコンパイル時の静的ディスパッチであり、この手法の名前を作り出しました:静的ポリモーフィズム.

正直、慣れるまで時間がかかりましたが、(6)行目の静的ポリモーフィズムの適用は至って簡単です。

最後に、動的ポリモーフィズムと静的ポリモーフィズムを簡単に比較したいと思います。

動的ポリモーフィズムと静的ポリモーフィズム

動的ポリモーフィズムは実行時に発生し、静的ポリモーフィズムはコンパイル時に発生します。動的ポリモーフィズムでは通常、実行時にポインターの間接化が必要ですが (「C++ での仮想関数、Vtable、および VPTR のわかりやすい解説」の記事を参照)、静的ポリモーフィズムでは実行時にパフォーマンス コストが発生しません。確かに、妙に繰り返されるテンプレート パターン (CRTP) のイディオムの内部に「curious」という名前が付けられているのには理由があります。初心者にとって、イディオムを理解するのは非常に困難です。では、何を使用すればよいでしょうか?

まず第一に、仮想ディスパッチのコストを過大評価しないでください。ほとんどの場合、それらは無視できます。詳細については、優れた論文「C++ パフォーマンスに関するテクニカル レポート」を参照してください。かなり古いですが、セクション 5.3.3 に、仮想関数呼び出しの追加コストに関する興味深い数値があります。それでもパフォーマンスが気になる場合は、対策が 1 つしかありません。それは、測定することです。パフォーマンス テストをバージョン管理下に置き、セットアップでハードウェア、コンパイラ、またはコンパイラのバージョンが変更された場合は、以前のパフォーマンス数値が無効になるため、常にテストを再実行してください。

結局のところ、コードは書かれたものよりも読まれた方がはるかに多いのです。したがって、チームが最も使い慣れている手法を使用する必要があります。

次は?

ミックスインは Python でよく使われる手法です。複数の継承を使用して、クラスの動作を変更できます。 CRTP のおかげで、C++ にも mixin があります。次回の投稿でそれらについて読んでください。