動的および静的ポリモーフィズム

ポリモーフィズムとは、異なる型が同じインターフェイスをサポートするという特性です。 C++ では、動的ポリモーフィズムと静的ポリモーフィズムを区別します。

これで、テンプレートに関する基本、詳細、およびテクニックが完了しました。テンプレートを使用したデザインについて書きましょう。ポリモーフィズムには多くの種類がありますが、1 つの側面に集中したいと思います。ポリモーフィズムのディスパッチは実行時またはコンパイル時に行われますか?ランタイム ポリモーフィズムは C++ のオブジェクト指向と仮想関数に基づいており、コンパイル時ポリモーフィズムはテンプレートに基づいています。

どちらのポリモーフィズムにも長所と短所があり、それについては次の投稿で説明します。

動的ポリモーフィズム

ここに重要な事実があります。動的ポリモーフィズムは実行時に行われ、オブジェクト指向に基づいており、インターフェイスとクラス階層の実装を分離することができます。遅延バインディング、動的ディスパッチ、または実行時のディスパッチを取得するには、仮想性と、ポインターや参照などの間接化という 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; // (3) MessageSeverity& messRef2 = messWarn; // (4) MessageSeverity& messRef3 = messFatal; // (5) writeMessageReference(messRef1); writeMessageReference(messRef2); writeMessageReference(messRef3); std::cerr << '\n'; MessageSeverity* messPoin1 = new MessageInformation; // (6) MessageSeverity* messPoin2 = new MessageWarning; // (7) MessageSeverity* messPoin3 = new MessageFatal; // (8) writeMessagePointer(messPoin1); writeMessagePointer(messPoin2); writeMessagePointer(messPoin3); std::cout << '\n'; }

関数 writeMessageReference (1 行目) または writeMessagePointer (2 行目) MessageSeverity 型のオブジェクトへの参照またはポインタが必要です . MessageSeverity から公に派生したクラス MessageInformation など 、 MessageWarning 、または MessageFatal いわゆるリスコフ置換原理をサポートします。これは MessageInformationMessageWarning 、または MessageFatal MessageSeverity です .

これがプログラムの出力です。

なぜメンバー関数 writeMessage 基本クラスではなく、派生クラスが呼び出されますか?ここで、遅延バインディングが開始されます。次の説明は、(3) 行から (8) 行に適用されます。簡単にするために、(6) 行目 MessageSeverity* messPoin1 = new MessageInformation についてだけ書きます。 . messPoint1 には基本的に 2 つのタイプがあります。静的型 MessageSeverity および動的タイプ MessageInformation .静的型 MessageSeverity そのインターフェースと動的タイプ MessageInformationを表します その実装のために。静的型はコンパイル時に使用され、動的型は実行時に使用されます。実行時に、messPoint1 のタイプは MessageInformation です。;したがって、仮想関数 writeMessage MessageInformation の と呼ばれます。繰り返しになりますが、動的ディスパッチには、ポインターや参照などの間接化と仮想性が必要です。

私はこの種のポリモーフィズムをコントラクト主導の設計と見なしています。 writeMessagePointer などの関数 MessageSeverity から公的に派生したものであることを各オブジェクトがサポートする必要がある .このコントラクトが満たされない場合、コンパイラは文句を言います。

契約主導の設計とは対照的に、行動主導の設計もあります。 静的ポリモーフィズムを使用します。

静的ポリモーフィズム

少し寄り道をしてみましょう。

Python では、正式なインターフェイスではなく、動作に注意を払います。このアイデアはダックタイピングとしてよく知られています。簡単に言うと、表現はジェームズ・ウィットコム・ライリーズの詩に戻ります:ここにあります:

「アヒルのように歩き、アヒルのように泳ぎ、アヒルのように鳴く鳥を見るとき、私はその鳥をアヒルと呼びます。」

どういう意味ですか?関数 acceptOnlyDucks を想像してみてください アヒルのみを引数として受け入れます。 C++ などの静的に型付けされた言語では、Duck から派生したすべての型 関数を呼び出すために使用できます。 Python では、Duck のように動作するすべての型 を使用して関数を呼び出すことができます。より具体的にするために。鳥が Duck, のように振る舞う場合 Duck です . Python では、この動作を非常によく説明するためによく使われることわざがあります。

許可を求めるのではなく、許しを求めてください。

Duck の場合、これは関数 acceptsOnlyDucks を呼び出すことを意味します。 鳥と一緒に最高のものを願っています。何か問題が発生した場合は、except 句で例外をキャッチします。通常、この戦略は Python で非常にうまく機能し、非常に高速です。

さて、これで私の回り道は終わりです。この C++ の投稿で、なぜ私がダック タイピングについて書いたのか疑問に思われるかもしれません。理由は至って簡単です。テンプレートのおかげで、C++ でダック タイピングを行うことができます。

これは、以前のプログラム disptachStaticPolymorphism.cpp をリファクタリングできることを意味します ダックタイピングを使用。

// duckTyping.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{ void writeMessage() const { std::cerr << "unexpected" << '\n'; } }; struct MessageInformation { void writeMessage() const { std::cerr << "information" << '\n'; } }; struct MessageWarning { void writeMessage() const { std::cerr << "warning" << '\n'; } }; struct MessageFatal: MessageSeverity{}; template <typename T> void writeMessage(T& messServer){ // (1) writeElapsedTime(); messServer.writeMessage(); } int main(){ std::cout << '\n'; MessageInformation messInfo; writeMessage(messInfo); MessageWarning messWarn; writeMessage(messWarn); MessageFatal messFatal; writeMessage(messFatal); std::cout << '\n'; }

関数テンプレート writeMessage (1 行目) ダックタイピングを適用します。 writeMessage すべてのオブジェクト messServer がメンバー関数 writeMessage をサポートしていると仮定します .そうでない場合、コンパイルは失敗します。 Python との主な違いは、C++ ではコンパイル時にエラーが発生するのに対し、Python では実行時にエラーが発生することです。最後に、これがプログラムの出力です。

関数 writeMessage ポリモーフィックに動作しますが、タイプ セーフではなく、エラーが発生した場合に読み取り可能なエラー メッセージを書き込みます。少なくとも、C++20 の概念に関する最後の問題は簡単に修正できます。概念については、私の以前の投稿で概念について詳しく読むことができます。次の例では、 MessageServer という概念を定義して使用しています。 (1行目).

// duckTypingWithConcept.cpp

#include <chrono>
#include <iostream>

template <typename T> // (1)
concept MessageServer = requires(T t) {
 t.writeMessage();
};

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{
 void writeMessage() const {
 std::cerr << "unexpected" << '\n';
 }
};

struct MessageInformation {
 void writeMessage() const { 
 std::cerr << "information" << '\n';
 }
};

struct MessageWarning {
 void writeMessage() const { 
 std::cerr << "warning" << '\n';
 }
};

struct MessageFatal: MessageSeverity{}; 

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

int main(){

 std::cout << '\n';
 
 MessageInformation messInfo;
 writeMessage(messInfo);
 
 MessageWarning messWarn;
 writeMessage(messWarn);

 MessageFatal messFatal;
 writeMessage(messFatal);
 
 std::cout << '\n';

}

MessageServer の概念 (1 行目) では、オブジェクト t が必要です。 タイプ T の 呼び出しをサポートする必要があります t.writeMessage. 行 (2) は、関数テンプレート writeMessage の概念を適用します。 .

次は?

ここまでは、テンプレートのポリモーフィックな振る舞いについてだけ書いてきましたが、静的ポリモーフィズムについては書きませんでした。これは次の投稿で変わります。いわゆる CRTP イディオムを紹介します。 CRTP は C の略です 不思議にR 繰り返し T テンプレート P attern とは、クラス Derived を継承する C++ の手法を意味します。 テンプレートクラス Base から そして Base Derived を持っています テンプレート パラメータとして:

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

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