CRTP 静的多型。 ミックスイン トピックに関する考察

この投稿では、C ++の静的ポリモーフィズム、それに基づいたアーキテクチャソリューションのトピックについて考察します。 興味深いイディオム-CRTPを考えてください。 その使用例をいくつか示します。 特に、 MixInクラスの概念を検討します。 私は自分の知識を整理するために書いていますが、おそらくあなたはあなた自身のために面白い何かを見つけることができます。



はじめに



ご存知のように、C ++はマルチパラダイム言語です。 手続き型スタイルで記述でき、オブジェクト指向プログラミングのサポートを提供する言語コンストラクトを使用できます。テンプレートを使用すると、プログラミング、 STL 、および新しい言語機能( ラムダ、std ::関数、std :: bind )を一般化でき、関数型スタイルで記述できますランタイムでは、テンプレートのメタプログラミングはコンパイル時の純粋な形の関数型プログラミングです。

実際の大規模なプログラムでは、これらのすべての手法、クラスの概念を使用して実装されたオブジェクト指向のパラダイム、オープンインターフェイスとクローズド実装(カプセル化)、継承、仮想関数を介して実装された動的ポリモーフィズムが混在している可能性が高いという事実にもかかわらず、間違いなく最も広く使用されています。



ただし、動的ポリモーフィズムは無料ではありません。 仮想関数を呼び出すのに必要な時間は、特定の状況、たとえば多くの多態性オブジェクトを処理するサイクルなどではあまり長くないという事実にもかかわらず、このようなソリューションのオーバーヘッドは通常の関数と比較して顕著になります。



静的多型



動的なポリモーフィズムはランタイムおよび明示的なインターフェイスのポリモーフィズムですが、静的なポリモーフィズムはコンパイル時間および暗黙的なインターフェイスのポリモーフィズムです。 これが何を意味するのか見てみましょう。

次のコードを見る



void process(base* b) { b->prepare(); b->work(); ... }
      
      







プロセス()関数に渡されるポインターは、 ベースインターフェイス(継承)を実装するオブジェクトを指している必要があり、 prepare()およびwork()関数の実装は、オブジェクトに応じてプログラムの実行中に選択されます基本型から派生したbを示します。



次のコードを検討する場合:



 template<typename T> void process(T t) { t.prepare(); t.work(); }
      
      





次に、最初に、 T型のオブジェクトにはprepare()およびwork()関数が必要であり、2番目に、これらの関数の実装は、派生したTの実際の型に基づいてコンパイル時に選択されると言えます

ご覧のように、アプローチのすべての違いについて、両方のタイプのポリモーフィズムの主な(実用的な観点からの)共通の特徴は、異なるタイプのオブジェクトを操作するときに、上記の要件を満たしていれば、クライアントコードを変更する必要がないことです。



すべてが素晴らしいので、原則として、コードは複雑ではなく、実行時のオーバーヘッドは平準化されます。動的多型を静的多型に完全に置き換えてみませんか? 残念ながら、通常そうであるように、物事はそれほど単純ではありません。 静的多型には、多くの主観的な欠点と客観的な欠点があります。 たとえば、主観的なことは、特に大規模なプロジェクトでは、明示的なインターフェイスが開発者の生活を単純化することが多いという事実です。 目の前にクラスを持つヘッダーファイルがある-実装する必要があるインターフェイスと、実装する必要がある関数とこのコードを機能させる方法についてテンプレート関数のコードを調べるよりもはるかに便利です。 また、このコードは長い間書かれており、現在、この部分またはその部分で何が意味されているのかを尋ねる人がいないことを想像してください。



しかし、客観的な理由は、インスタンス化後、テンプレートクラス(関数)がさまざまなタイプを持ち、多くの場合互いに相互に接続されていないという事実に何らかの形で減らすことができます。

なぜこれが悪いのですか? 追加のトリックのないこれらのタイプのオブジェクト( boost :: variant、boost :: tuple、boost :: any、boost :: fusionなどを参照)は1つのコンテナーに入れることができないため、バッチ処理されます。 たとえば、実行中に、「Strategy」または「Status」の実装のフレームワーク内で、オブジェクト(クラスのメンバー)を別のタイプのオブジェクトに置き換えることはできません。 これらのパターンは、クラス階層なしで他の方法で実装することもできますが、たとえば、 std ::関数または関数へのポインターのみを使用しますが、制限は表面にあります。



しかし、誰も私たちに一つのパラダイムを厳守することを強制しません。 最も強力で柔軟性があり、興味深いソリューションは、これら2つのアプローチの接合点、OOPパラダイムと汎用パラダイムの接合点で発生します。 CRTPイディオムは、このようなパラダイムの合併の一例にすぎません。



CRTP



CRTP(Curiously Recurring Template Pattern)は、クラスが基本テンプレートクラスから継承し、それ自体を基本クラスのテンプレートパラメータとして継承するという設計上のイディオムです。 複雑に聞こえますが、コードではかなり単純に見えます。



 template <class T> class base{}; class derived : public base<derived> {};
      
      







これにより何が得られますか? この設計により、基本クラスから派生クラスにアクセスできます。



 template<typename D> struct base { void foo() {static_cast<D*>(this)->bar();} }; struct derived : base<derived> { void bar(); };
      
      







そして、そのようなコミュニケーションの可能性は、次に、いくつかの興味深い可能性を開きます。



明示的なインターフェース



静的多型に関する章では、明示的なインターフェイスの欠如を静的多型の主観的な欠点と呼びました。 このトピックについて議論することはできますが、 CRTPを使用して明示的なインターフェースを決定することは、 なんらかの方法で難しくありません。 実際、基本クラスからこれらの関数を呼び出すことにより、必要なインターフェイス関数のセットを定義できます。



 template<typename D> struct base_worker { void work() {static_cast<D*>(this)->work_impl();} void prepare() {static_cast<D*>(this)->prepare_impl();} }; struct some_concrete_worker : base_worker<some_concrete_worker> { void work_impl(); //     void prepare_impl(); //    }; template<typename Worker> void polymorhic_work(const Worker& w) { w.prepare(); w.work(); }; int main() { some_concrete_worker w1; some_concrete_worker_2 w2; polymorhic_work(w1); //     polymorhic_work(w2); //  prepare_impl()  work_impl()  w1  w2 }
      
      







このような設計の助けを借りて、1人の開発者(アーキテクト)が特定のクラスのセットのインターフェースを指定でき、残りのプログラマーはこのインターフェースを実装するときにガイドされるものを持っています。 ただし、 CRTP機能これに限定されません。



ミックスイン



MixInは、クラス(インターフェイス、モジュールなど)が別のクラスに「ミックス」できる機能を実装する設計手法です。 通常、 MixInクラスは単独では使用されません。 この手法はC ++に固有のものではなく、他の一部の言語では言語構成のレベルでサポートされています。

C ++では、 MixInのネイティブサポートはありませんが、このイディオムはCRTPを使用して完全に実装できます。

たとえば、 MixInクラスは、シングルトンまたはカウントオブジェクト参照の機能を実装できます。 そして、そのようなクラスを使用するには、テンプレートパラメータとして「自分」を使用してクラスから継承するだけで十分です。



 template<typename D> struct singleton{...}; class my_class : public singleton<my_class>{...};
      
      







ここにCRTPがあるのはなぜですか? 必要な機能を実装するクラスから継承するだけではどうですか?



 struct singleton{...}; class my_class : singleton{...};
      
      







事実、 MixIn内では、継承されたクラスの関数(コンストラクターへのシングルトンの場合)にアクセスする必要があり、ここでCRTP役立ちます。 そして、シングルトンを使用した例が非常にフェッチされているように見える場合(実際、今日シングルトンを使用しているのは誰ですか?)、次に、より現実に近い2つの例を見つけます。



Enable_shared_from_this


MixIn構造(ブースト)std :: enable_shared_from_thisを使用すると、新しい所有権グループを作成せずにオブジェクトのshared_ptrを取得できます。



 struct bad { std::shared_ptr<bad> get() {return std::shared_ptr<bad>(this);} };
      
      







この場合、 bad :: get()関数を使用して取得した各shared_ptrは、オブジェクトの新しい所有権グループを開き、 shared_ptrを破棄するときに、オブジェクトのdeleteが複数回呼び出されます。



このようにするのは正しいことです。



 struct good : std::enable_shared_from_this<good> { std::shared_ptr<good> get() { return shared_from_this(); //    //  enable_shared_from_this } };
      
      







この補助構造は、ほぼ次のように構成されています。



 template<typename T> struct enable_shared { weak_ptr<T> t_; enable_shared() { t_ = weak_ptr<T>(static_cast<T*>(this)); } shared_ptr<T> shared_from_this() { return shared_ptr<T>(t_); } };
      
      







ご覧のとおり、ここでCRTPを使用すると、基本クラスは派生クラスの型を「参照」して、それへのポインターを返すことができます。



ミックスイン機能


MixIn機能は、一部のクラス内に含める必要はありません。 時々、それを自由な関数として実装することが可能です。 例として、「==」演算子が定義されているすべてのクラスに「!=」演算子を実装します。



 template<typename D> struct non_equalable{}; template<typename D> bool operator != (const non_equalable<D>& lhs, const non_equalable<D>& rhs) { return !(static_cast<const D&>(lhs) == static_cast<const D&>(rhs)); }
      
      







ご覧のとおり、inside operator!= non_equalableは派生型で定義された「==」演算子を使用できるという事実を使用します。

このMixInは次のように使用できます。



 struct some_struct : non_equalable<some_struct> { some_struct(int w) : i_(w){} int i_; }; bool operator == (const some_struct& lhs, const some_struct& rhs) { return lhs.i_ == rhs.i_; } int main() { some_struct s1(3); some_struct s2(4); std::cout << (s1 != s2) << std::endl; }
      
      







他の方法でミックスイン



ゲーム宇宙船のクラスの実装を書いていると想像してください。 いくつかの点を除き、当社の船は同じアルゴリズムに従います。たとえば、残りの燃料と現在の速度を計算するメカニズムは船ごとに異なります。 パターンテンプレートメソッドの古典的な実装(およびこれは)は次のようになります。



 class space_ship { public: // ... void move() { if(!fuel()) return; int current_speed = speed(); // further actions ... } virtual ~space_ship(){} private: virtual bool fuel() const = 0; virtual int speed() const = 0; }; class interceptor : public space_ship { public: // ... private: bool fuel() const { ... } int speed() const { ... } }; class other_ship : public space_ship { ... }; class other_ship_2 : public space_ship { ... }; // …
      
      







次に、 CRTPを適用してみてください。



 template<typename D> class space_ship { public: void move() { if(!static_cast<D*>(this)->fuel()) return; int current_speed = static_cast<D*>(this)->speed(); // ... } }; class interceptor : public space_ship<interceptor> { public: bool fuel() const; int speed() const; };
      
      







この実装では、仮想関数を取り除き、コード自体が短くなりました(基本クラスで純粋に仮想関数を記述する必要はありません)。



このアプローチを使用したMixInのコンセプトは、逆さまになっています。 主な作業は基本クラスで行われ、派生クラスから追加(異なる)機能を「ミックス」します。



この設計手法とMixin 'ah全般に注意を向けたいと思います。 宇宙船やシングルトンの人工的な例に恥ずかしがらないでください。 実際のタスクでは、このアプローチにより、非常に柔軟なアーキテクチャを構築し、コードの繰り返しを避け、機能を小さなクラスにローカライズし、その後必要なミックスに「ミックス」できます。 特に、異なるタイプの多くのオブジェクトのバッチ処理を可能にするツールと協力して輝き始めます( boost :: fusionを参照)。



ミックスインバリエーション



ソフトウェア開発の主な定理( FTSE )には、「間接的なレベルを追加することで問題を解決できます。」 これをどのようにCRTP MixInに適用できるか見てみましょう。

前の章の「明示的なインターフェイス」と「MixInの逆」でお気づきかもしれませんが、派生クラスでパブリック関数を使用しました。 一般的に言えば、カプセル化に違反するため、これはあまり良くありません。 ユーザーが直接呼び出すことを意図していない「突出」関数があることがわかります。

この問題は、ベースクラスを派生クラスのフレンドにすることで解決できます。 その後、これらの関数をprivateセクションに追加できますが、いくつかの基本的なMixInから継承する必要があると想像してください。 すべての基本クラスを友達にする必要があります。 この問題を包括的に解決するために、またいくつかの古いコンパイラでコンパイルを確実にするために、新しいレベルのインダイレクションを導入できます。 これは、関数がデータベースからの呼び出しを派生クラスにリダイレクトする構造です。



 struct access { template<typename Impl> static void on_handle_connect(Impl* impl) {impl->handle_connect();} template<typename Impl> static void on_handle_response(Impl* impl) {impl->handle_response();} };
      
      







ここで、基底クラスから、導関数の関数ではなく、中間構造の関数を呼び出します。



 template<typename D> struct connection_handler { // ... void on_connection() { access::on_handle_connect(static_cast<D*>(this)); } }; template<typename D> struct response_handler { // ... void on_response() { access::on_handle_response(static_cast<D*>(this)); } };
      
      







派生クラスでは、友人をアクセス構造のみにするだけで十分です。



 class combined_handler : public connection_handler<worker>, public response_handler<worker> { private: friend struct access; void handle_connect(){ std::cout << __PRETTY_FUNCTION__ << std::endl; } void handle_response(){ std::cout << __PRETTY_FUNCTION__ << std::endl; } };
      
      







このアプローチのその他の利点には、基本クラスが派生クラス、特にどの特定の関数から呼び出されるべきかについて「知る」ことをやめるという事実と、疎結合システムが通常、強結合システムよりも柔軟であるという事実と、派生クラスへのすべての呼び出しは1つの場所(アクセス構造内)に収集されるため、他の作業を実行する派生クラスの機能から視覚的に簡単に分離できます。

よくあることですが、デメリットは設計決定の複雑さです。 したがって、私は尾とたてがみにそのようなスキームを使用することを決して勧めませんが、それについての考えを持つことは不必要ではないように思えます。



All Articles