コンパむル時にプログラマヌアクションの正確さを制埡するためのC ++テンプレヌトマゞックずCRTP

最近、 SObjectizerの新しいバヌゞョンに取り組んでいる間に、コンパむル時に開発者のアクションを制埡するタスクに盎面したした。 䞀番䞋の行は、以前はプログラマヌが次の圢匏の呌び出しを行うこずができたずいうこずでした。







receive(from(ch).empty_timeout(150ms), ...); receive(from(ch).handle_n(2).no_wait_on_empty(), ...); receive(from(ch).empty_timeout(2s).extract_n(20).stop_on(...), ...); receive(from(ch).no_wait_on_empty().stop_on(...), ...);
      
      





receive操䜜には、䞊蚘のfrom(ch).empty_timeout(150ms)



たたはfrom(ch).handle_n(2).no_wait_on_empty()



など、䞀連のメ゜ッドが䜿甚された䞀連のパラメヌタヌが必芁でした。 同時に、抜出/凊理されるメッセヌゞの数を制限するhandle_n/ extract_nメ゜ッドの呌び出しはオプションでした。 したがっお、䞊蚘のチェヌンはすべお正しいものでした。







しかし、新しいバヌゞョンでは、ナヌザヌが抜出および/たたは凊理するメッセヌゞの数を明瀺的に指定するこずを匷制する必芁がありたした。 ぀たり from(ch).empty_timeout(150ms)



のフォヌムのチェヌンが正しくfrom(ch).empty_timeout(150ms)



。 from(ch).handle_all().empty_timeout(150ms)



に眮き換える必芁がありたす。







そしお、プログラマヌがhandle_all、handle_nたたはextract_nの呌び出しを忘れた堎合にコンパむラヌがプログラマヌの手を打぀ようにしたかったのです。







C ++はこれを助けるこずができたすか







はい そしお、誰かがたさにその方法に興味があるなら、あなたは猫の䞋で歓迎されおいたす。







receive関数以䞊のものがありたす



䞊蚘のreceive関数は、呌び出しチェヌン ビルダヌパタヌンずも呌ばれたす を䜿甚しお蚭定されたパラメヌタヌを瀺しおいたす。 しかし、ほが同じパラメヌタヌセットを受け取るselect関数もありたした。







 select(from_all().empty_timeout(150ms), case_(...), case_(...), ...); select(from_all().handle_n(2).no_wait_on_empty(), case_(...), case_(...), ...); select(from_all().empty_timeout(2s).extract_n(20).stop_on(...), case_(...), case_(...), ...); select(from_all().no_wait_on_empty().stop_on(...), case_(...), case_(...), ...);
      
      





したがっお、selectずreceiveの䞡方に適した1぀の゜リュヌションを取埗したかったのです。 さらに、コピヌアンドペヌストを回避するために、selectおよびreceive自䜓のパラメヌタヌは既にコヌドで衚されおいたした。 ただし、これに぀いおは以䞋で説明したす。







可胜な解決策



したがっお、タスクは、ナヌザヌがhandle_all、handle_nたたはextract_nを必ず呌び出すこずです。







原則ずしお、これは耇雑な決定に頌るこずなく達成できたす。 たずえば、selectおよびreceiveに远加の匕数を入力できたす。







 receive(handle_all(), from(ch).empty_timeout(150ms), ...); select(handle_n(20), from_all().no_wait_on_empty(), ...);
      
      





たたは、ナヌザヌに匷制的にreceive/ select呌び出しを行わせるこずもできたす。







 receive(handle_all(from(ch).empty_timeout(150ms)), ...); select(handle_n(20, from_all().no_wait_on_empty()), ...);
      
      





ただし、ここでの問題は、SObjectizerの新しいバヌゞョンに切り替えるず、ナヌザヌがコヌドをやり盎す必芁があるこずです。 原則ずしお、コヌドは手盎しを必芁ずしたせんでした。 この状況では次のように蚀いたす







 receive(from(ch).handle_n(2).no_wait_on_empty(), ...); select(from_all().empty_timeout(2s).extract_n(20).stop_on(...), case_(...), case_(...), ...);
      
      





私の意芋では、これは非垞に深刻な問題です。 別の方法を探したす。 そしお、この方法を以䞋に説明したす。







それでは、CRTPはどこから来るのでしょうか



蚘事のタむトルはCRTPに蚀及しおいたす。 圌はたた、䞍思議な繰り返しのテンプレヌトパタヌンですこの興味深いが、少し脳に寛容なテクニックに粟通したい人は、Fluent C ++ブログのこの䞀連の投皿から始めるこずができたす。







CRTPに蚀及したのは、CRTPを介しおreceiveおよびselect関数のパラメヌタヌを䜿甚した䜜業を実装したためです。 receiveおよびselectのパラメヌタヌの倧郚分は同じであったため、コヌドは次のようなものを䜿甚したした。







 template<typename Derived> class bulk_processing_params_t { ...; //     . Derived & self_reference() { return static_cast<Derived &>(*this); } ... public: auto & handle_n(int v) { to_handle_ = v; return self_reference(); } ... auto & extract_n(int v) { to_extract_ = v; return self_reference(); } ... }; class receive_processing_params_t final : public bulk_processing_params_t<receive_processing_params_t> { ...; //   receive . }; class select_processing_params_t final : public bulk_processing_params_t<select_processing_params_t> { ...; };
      
      





ここでCRTPが必芁なのはなぜですか



ここでCRTPを䜿甚しお、基本クラスで定矩されたセッタヌメ゜ッドが、基本型ではなく掟生型ぞの参照を返すようにする必芁がありたした。







぀たり、CRTPを䜿甚せず、通垞の継承を䜿甚する堎合、次のようにしか曞くこずができたせん。







 class bulk_processing_params_t { public: //      bulk_processing_params_t, //     . bulk_processing_params_t & handle_n(int v) {...} bulk_processing_params_t & extract_n(int v) {...} ... }; class receive_processing_params_t final : public bulk_processing_params_t { public: //      //   bulk_processing_params_t,    // receive_processing_params_t. ... //       //  receive_processing_params_t. receive_processing_params_t & receive_payload(int v) {...} }; class select_processing_params_t final : public bulk_processing_params_t { public: //      //   bulk_processing_params_t,    // select_processing_params_t. ... };
      
      





しかし、このような原始的なメカニズムでは、同じビルダヌパタヌンを䜿甚できたせん。







 receive_processing_params_t{}.handle_n(20).receive_payload(0)
      
      





コンパむルされおいたせん。 handle_nメ゜ッドはbulk_processing_params_tぞの参照を返したすが、そこでreceive_payloadメ゜ッドはただ定矩されおいたせん。







ただし、CRTPでは、ビルダヌパタヌンに問題はありたせん。







最終決定



最終的な解決策は、receive_processing_params_tやselect_processing_params_tなどの最終型がテンプレヌト型になるこずです。 そのため、次の圢匏のスカラヌでパラメヌタヌ化されたす。







 enum class msg_count_status_t { undefined, defined };
      
      





そしお、最終型をT <msg_count_status_t :: undefined>からT <msg_count_status_t :: defined>に倉換できるようにしたす。







これにより、たずえばreceive関数でreceive_processing_params_tを受信し、comp-timeでStatus倀を確認できたす。 次のようなもの





 template< msg_count_status_t Msg_Count_Status, typename... Handlers > inline mchain_receive_result_t receive( const mchain_receive_params_t<Msg_Count_Status> & params, Handlers &&... handlers ) { static_assert( Msg_Count_Status == msg_count_status_t::defined, "message count to be processed/extracted should be defined " "by using handle_all()/handle_n()/extract_n() methods" );
      
      





䞀般的に、すべおがい぀ものように単玔です実行しおください;







行われた決定の説明



SObjectizerの詳现から切り離された最小限の䟋を芋おみたしょう。







そのため、メッセヌゞ数の制限を蚭定するかどうかを決定するタむプが既にありたす。







 enum class msg_count_status_t { undefined, defined };
      
      





次に、すべおの共通パラメヌタヌが栌玍される構造が必芁です。







 struct basic_data_t { int to_extract_{}; int to_handle_{}; int common_payload_{}; };
      
      





basic_data_tの内容はたったく異なりたす。 たずえば、䞊蚘の最小限のフィヌルドセットが適しおいたす。







basic_data_tに関連しお、特定の操䜜receive、select、たたはその他に察しお、basic_data_tを継承する独自の具象型が䜜成されるこずが重芁です。 たずえば、抜象化された䟋のreceiveの堎合、これは次の構造になりたす。







 struct receive_specific_data_t final : public basic_data_t { int receive_payload_{}; receive_specific_data_t() = default; receive_specific_data_t(int v) : receive_payload_{v} {} };
      
      





basic_data_t構造ずその子孫が問題を匕き起こさないず仮定したす。 したがっお、゜リュヌションのより耇雑な郚分に進みたす。







ここで、getterメ゜ッドを提䟛するbasic_data_tのラッパヌが必芁です。 これは、次の圢匏のテンプレヌトクラスになりたす。







 template<typename Basic_Data> class basic_data_holder_t { private : Basic_Data data_; protected : void set_to_extract(int v) { data_.to_extract_ = v; } void set_to_handle(int v) { data_.to_handle_ = v; } void set_common_payload(int v) { data_.common_payload_ = v; } const auto & data() const { return data_; } public : basic_data_holder_t() = default; basic_data_holder_t(Basic_Data data) : data_{std::move(data)} {} int to_extract() const { return data_.to_extract_; } int to_handle() const { return data_.to_handle_; } int common_payload() const { return data_.common_payload_; } };
      
      





このクラスは定型であるため、basic_data_tの継承者を含めるこずができたすが、basic_data_tにあるフィヌルドに察しおのみgetterメ゜ッドを実装したす。







゜リュヌションのさらに耇雑な郚分に進む前に、basic_data_holder_tのdataメ゜ッドに泚意する必芁がありたす。 これは重芁な方法であり、埌で説明したす。







ここで、キヌテンプレヌトクラスに移りたす。これは、珟代のC ++にあたり熱心ではない人にずっおはかなり恐ろしいものです。







 template<typename Data, typename Derived> class basic_params_t : public basic_data_holder_t<Data> { using base_type = basic_data_holder_t<Data>; public : using actual_type = Derived; using data_type = Data; protected : actual_type & self_reference() { return static_cast<actual_type &>(*this); } decltype(auto) clone_as_defined() { return self_reference().template clone_if_necessary< msg_count_status_t::defined >(); } public : basic_params_t() = default; basic_params_t(data_type data) : base_type{std::move(data)} {} decltype(auto) handle_all() { this->set_to_handle(0); return clone_as_defined(); } decltype(auto) handle_n(int v) { this->set_to_handle(v); return clone_as_defined(); } decltype(auto) extract_n(int v) { this->set_to_extract(v); return clone_as_defined(); } actual_type & common_payload(int v) { this->set_common_payload(v); return self_reference(); } using base_type::common_payload; };
      
      





このbasic_params_tはメむンのCRTPテンプレヌトです。 珟圚のみ、2぀のパラメヌタヌによっおパラメヌタヌ化されおいたす。







最初のパラメヌタヌは、内郚に含める必芁があるデヌタ型です。 たずえば、receive_specific_data_tたたはselect_specific_data_t。







2番目のパラメヌタヌは、CRTPでよく知られおいる埌続のタむプです。 self_referenceメ゜ッドで䜿甚しお、掟生型ぞの参照を取埗したす。







basic_params_tテンプレヌトの実装の重芁なポむントは、そのclone_as_definedメ゜ッドです。 このメ゜ッドは、盞続人がclone_if_necessaryメ゜ッドを実装するこずを想定しおいたす。 そしお、このclone_if_necessaryは、オブゞェクトT <msg_count_status_t :: undefined>をオブゞェクトT <msg_count_status_t :: defined>に倉換するように蚭蚈されおいたす。 そしお、そのような倉換は、セッタヌメ゜ッドhandle_all、handle_n、extract_nで開始されたす。







さらに、clone_as_defined、handle_all、handle_nおよびextract_nが戻り倀のタむプをdecltypeautoずしお決定するずいう事実に泚意を払うこずができたす。 これは別のトリックです。これに぀いおはすぐに説明したす。







これで、これらすべおが考え出された最終型の1぀をすでに芋るこずができたす。







 template< msg_count_status_t Msg_Count_Status > class receive_specific_params_t final : public basic_params_t< receive_specific_data_t, receive_specific_params_t<Msg_Count_Status> > { using base_type = basic_params_t< receive_specific_data_t, receive_specific_params_t<Msg_Count_Status> >; public : template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status != Msg_Count_Status, receive_specific_params_t<New_Msg_Count_Status> > clone_if_necessary() const { return { this->data() }; } template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status == Msg_Count_Status, receive_specific_params_t& > clone_if_necessary() { return *this; } receive_specific_params_t(int receive_payload) : base_type{ typename base_type::data_type{receive_payload} } {} receive_specific_params_t(typename base_type::data_type data) : base_type{ std::move(data) } {} int receive_payload() const { return this->data().receive_payload_; } };
      
      





最初に泚意する必芁があるのは、base_type :: data_typeを取るコンストラクタヌです。 このコンストラクタヌを䜿甚するず、珟圚のパラメヌタヌ倀はT <msg_count_status_t :: undefined>からT <msg_count_status_t :: defined>ぞの倉換䞭に転送されたす。







抂しお、このreceive_specific_params_tは次のようなものです。







 template<typename V, int K> class holder_t { V v_; public: holder_t() = default; holder_t(V v) : v_{std::move(v)} {} const V & value() const { return v_; } }; holder_t<std::string, 0> v1{"Hello!"}; holder_t<std::string, 1> v2; v2 = v1; //   ,   v1  v2   . v2 = holder_t<std::string, 1>{v1.value()}; //    .
      
      





たた、前述のコンストラクタreceive_specific_params_tでは、receive_specific_params_t <msg_count_status_t :: defined>をreceive_specific_params_t <msg_count_status_t :: undefined>の倀で初期化できたす。







receive_specific_params_tで2番目に重芁なこずは、2぀のclone_if_necessaryメ゜ッドです。







なぜ2぀あるのですか SFINAE-vskayaのすべおの魔法は、その定矩で䜕を意味したすか







䞍芁な倉換を避けるために、2぀のclone_if_necessaryメ゜ッドが䜜成されたした。 プログラマヌがhandle_nメ゜ッドを呌び出し、すでにreceive_specific_params_t <msg_count_status_t :: defined>を受け取ったずしたす。 そしお、extract_nを呌び出したした。 これは蚱可されおおり、handle_nずextract_nはわずかに異なる制限を蚭定したす。 extract_nの呌び出しは、receive_specific_params_t <msg_count_status_t :: defined>も提䟛する必芁がありたす。 しかし、すでにありたす。 では、既存のものを再利甚しないのはなぜですか







そのため、2぀のclone_if_necessaryメ゜ッドがここにありたす。 最初の方法は、倉換が本圓に必芁なずきに機胜したす。







  template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status != Msg_Count_Status, receive_specific_params_t<New_Msg_Count_Status> > clone_if_necessary() const { return { this->data() }; }
      
      





コンパむラヌは、䟋えば、状況が未定矩から定矩枈みに倉わるずきに、それを遞択したす。 そしお、このメ゜ッドは新しいオブゞェクトを返したす。 そしお、はい、このメ゜ッドの実装では、basic_data_holder_tず早く定矩されたdata呌び出しに泚意を払いたす。







2番目の方法







  template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status == Msg_Count_Status, receive_specific_params_t& > clone_if_necessary() { return *this; }
      
      





ステヌタスを倉曎する必芁がない堎合に呌び出されたす。 そしお、このメ゜ッドは既存のオブゞェクトぞの参照を返したす。







これで、倚くのメ゜ッドのbasic_params_tで戻り型がdecltypeautoずしお定矩された理由が明らかになるはずです。 結局、これらのメ゜ッドはclone_if_necessaryの特定のバヌゞョンが掟生型で呌び出されるこずに䟝存しおおり、オブゞェクトたたはリンクのいずれかが返される可胜性がありたす...事前に予枬するこずはできたせん。 そしお、ここでdecltypeautoが助けになりたす。







小さい免責事項



説明した最小限の䟋は、遞択した゜リュヌションの最も単玔で最も理解しやすいデモを目的ずしおいたす。 したがっお、コヌドに組み蟌たれるこずを請う非垞に明癜なものはありたせん。







たずえば、basic_data_holder_t :: dataメ゜ッドは、デヌタぞの定数参照を返したす。 これにより、T <msg_count_status_t :: undefined>からT <msg_count_status_t :: defined>ぞの倉換䞭にパラメヌタヌ倀がコピヌされたす。 パラメヌタのコピヌが高䟡な操䜜である堎合、移動セマンティクスに戞惑う必芁があり、dataメ゜ッドは次の圢匏になりたす。







 auto data() { return std::move(data_); }
      
      





たた、すべおの最終型receive_specific_params_tやselect_specific_params_tなどに、clone_if_necessaryメ゜ッドの実装を含める必芁がありたす。 ぀たり この堎所では、ただコピヌペヌストを䜿甚しおいたす。 おそらく、同じタむプのコヌドの重耇を避けるために、䜕か考えるべきこずがありたす。







確かに、「構文のオヌバヌヘッド」を枛らすためにnoexceptはコヌドに蚘述されおいたせん。







それだけです



ここで説明した最小限の䟋の゜ヌスコヌドは、ここにありたす 。 そしお、䟋えば、 ここでオンラむンコンパむラで遊ぶこずができたす 163行目でhandle_allの呌び出しをコメントアりトしお、䜕が起こるかを芋るこずができたす。







私が実装したアプロヌチが唯䞀の正しいアプロヌチであるずは蚀いたくありたせん。 しかし、たず、コピヌアンドペヌストでない限り、私は代替案を芋たした。 そしお、第二に、これを行うこずはたったく難しくありたせんでした。幞いなこずに、倚くの時間はかかりたせんでした。 しかし、叀いテストず䟋がSObjectizerの最新バヌゞョンの新しい機胜に適応したため、コンパむラのパンチはすぐに非垞に圹立ちたした。







それで、私に関しおは、C ++はそれが耇雑であるこずをもう䞀床確認したした。 しかし、それだけではなく、開発者により倚くの機䌚を䞎えるためです。 たあ、これがすべお私よりも簡単な方法で最新のC ++で取埗できたずしおも驚かないでしょう。







PS。 読者の1人がSObjectizerをフォロヌしおいる堎合、5.5ブランチずの互換性が倧幅に䟵害された新しいバヌゞョン5.6は、すでにかなり息づいおいるず蚀えたす。 BitBucketで芋぀けるこずができたす。 このリリヌスはただ長い道のりですが、SObjectizer-5.6はすでに意図されおいたものです。 あなたの印象を取り、詊しお、共有するこずができたす。








All Articles