C ++または...の場合、完全な静的の欠如を後悔する

...テンプレートパラメータの値に応じて異なる内容でテンプレートクラスを埋める方法は?







むかしむかし、かなり前に、C ++で得られた経験を考慮して、D言語は「正しいC ++」として作成されるようになりました。 時間が経つにつれて、DはC ++よりも複雑で表現力豊かな言語になりました。 そして、すでにC ++はDをスパイし始めました。たとえば、 if constexpr



がDから直接借用しているif constexpr



、C ++ 17に登場しました。そのプロトタイプはD-shny static ifでした。







残念ながら、C ++のif constexpr



がDのstatic if



と同じ力を持たないstatic if



、これにはいくつかの理由がありますが、C ++のif constexpr



がC + +クラス。 これらのケースの1つについてお話ししたいと思います。







テンプレートクラスを作成する方法について説明します。テンプレートクラスの内容(メソッドの構成とメソッドのロジック)は、このテンプレートクラスに渡されたパラメーターに応じて変化します。 例は、SObjectizerの新しいバージョンを開発した経験から、実生活から取られています。







解決すべき課題



メッセージオブジェクトを保存するための「スマートポインタ」の賢いバージョンを作成する必要があります。 次のように書くことができます:







 message_holder_t<my_message> msg{ new my_message{...} }; send(target, msg); send(another_target, msg);
      
      





このmessage_holder_t



クラスのmessage_holder_t



isは、考慮すべき3つの重要な要素があることです。







継承されるメッセージの種類は何ですか?



message_holder_t



をパラメーター化するメッセージのタイプは、2つのグループに分けられます。 最初のグループは、特別なベースタイプmessage_t



を継承するmessage_t



です。 例:







 struct so5_message final : public so_5::message_t { int a_; std::string b_; std::chrono::milliseconds c_; so5_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} };
      
      





この場合、内部のmessage_holder_tには、このタイプのオブジェクトへのポインターのみを含める必要があります。 同じポインターがゲッターメソッドで返される必要があります。 つまり、 message_t



の継承者の場合、次のようになります。







 template<typename M> class message_holder_t { intrusive_ptr_t<M> m_msg; public: ... const M * get() const noexcept { return m_msg.get(); } };
      
      





2番目のグループは、 message_t



から継承されない任意のユーザータイプのmessage_t



です。 例:







 struct user_message final { int a_; std::string b_; std::chrono::milliseconds c_; user_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} };
      
      





SObjectizerのこれらのタイプのインスタンスは、それ自体では送信されませんが、特別なラッパーuser_type_message_t<M>



で囲まれています。 user_type_message_t<M>



ラッパーは、既にmessage_t



から継承さmessage_t



ます。 したがって、そのような型の場合、 message_holder_t



はその中にuser_type_message_t<M>



へのポインターを含む必要があり、ゲッターメソッドはMへのポインターを返す必要があります。







 template<typename M> class message_holder_t { intrusive_ptr_t<user_type_message_t<M>> m_msg; public: ... const M * get() const noexcept { return std::addressof(m_msg->m_payload); } };
      
      





メッセージの耐性または可変性



2番目の要因は、メッセージを不変および可変に分割することです。 メッセージが不変(およびデフォルトでは不変)である場合、getterメソッドはメッセージへの定数ポインターを返す必要があります。 また、可変の場合、ゲッターは非定数ポインターを返す必要があります。 つまり 次のようなものでなければなりません:







 message_holder_t<so5_message> msg1{...}; //  . const int a = msg1->a_; // OK. msg1->a_ = 0; //     ! message_holder_t<mutable_msg<user_message>> msg2{...}; //  . const int a = msg2->a_; // OK. msg2->a_ = 0; // OK.
      
      





shared_ptr vs unique_ptr



3番目の要因は、スマートポインターとしてのmessage_holder_t



の動作のロジックです。 むかしむかし、 std::shared_ptr



ように振る舞う必要があります。 同じメッセージインスタンスを参照する複数のmessage_holdersを持つことができます。 そして、一度std::unique_ptr



ように振る舞うはずです。 メッセージインスタンスを参照できるmessage_holderインスタンスは1つだけです。







デフォルトでは、 message_holder_t



の動作は、メッセージの可変性/不変性に依存する必要があります。 つまり 不変のメッセージでは、 message_holder_t



std::shared_ptr



ように動作し、 std::unique_ptr



ような可変std::unique_ptr









 message_holder_t<so5_message> msg1{...}; message_holder_t<so5_message> msg2 = msg; // OK. message_holder_t<mutable_msg<user_message>> msg3{...}; message_holder_t<mutable_msg<user_message>> msg4 = msg3; // !  ! message_holder_t<mutable_msg<user_message>> msg5 = std::move(msg3); // OK.
      
      





しかし、人生は複雑なものなので、 message_holder_t



動作を手動で設定できるようにする必要もあります。 これにより、unique_ptrのように動作する不変メッセージのmessage_holderを作成できます。 そして、shared_ptrのように動作する可変メッセージのmessage_holderを作成できます。







 using unique_so5_message = so_5::message_holder_t< so5_message, so_5::message_ownership_t::unique>; unique_so5_message msg1{...}; unique_so5_message msg2 = msg1; // !  ! unique_so5_message msg3 = std::move(msg); // OK,   msg3. using shared_user_messsage = so_5::message_holder_t< so_5::mutable_msg<user_message>, so_5::message_ownership_t::shared>; shared_user_message msg4{...}; shared_user_message msg5 = msg4; // OK.
      
      





したがって、 message_holder_t



がshared_ptrのように機能する場合、通常のコンストラクターと割り当て演算子のセット(コピーと移動の両方)が必要です。 さらに、定数メソッドmake_reference



が必要make_reference



。このメソッドは、 message_holder_t



内に格納されているポインターのコピーを返します。







ただし、 message_holder_t



がunique_ptrとして機能する場合は、コンストラクタとコピー演算子を禁止する必要があります。 make_reference



メソッドは、 message_holder_t



オブジェクトからポインターをmake_reference



する必要がありますmake_reference



を呼び出した後make_reference



元のmessage_holder_t



は空のままにしておく必要があります。







もう少しフォーマル



そのため、テンプレートクラスを作成する必要があります。







 template< typename M, message_ownership_t Ownership = message_ownership_t::autodetected> class message_holder_t {...};
      
      





どれ:









リストの最後の2つの項目は、所有権パラメーター(およびautodetected



値がautodetected



場合のメッセージの可変性)によって決定されます。







決定方法



このセクションでは、最終的なソリューションを構成するすべてのコンポーネントを検討します。 さて、結果のソリューション自体。 気が散る詳細がすべてクリアされたコードフラグメントが表示されます。 誰かが実際のコードに興味があるなら、 ここでそれを見ることができます







免責事項



以下に示すソリューションは、美、理想、またはロールモデルのふりをするものではありません。 締め切りのプレッシャーの下、短時間で発見、実装、テスト、文書化されました。 おそらく、もっと時間があり、解決策の検索にもっと多くの時間を費やしたとしたら 若い 現代のC ++開発者にとって賢明で知識が豊富なため、よりコンパクトでシンプルで理解しやすいものになります。 しかし、それが起こったとき、それは起こりました...「ピアニストを撃つな」、一般に。







手順のシーケンスと既製のテンプレートマジック



そのため、いくつかのメソッドのセットを持つクラスが必要です。 これらのキットの内容はどこかからのものでなければなりません。 どこから?







Dでは、 static if



を使用し、さまざまな条件に応じてクラスのさまざまな部分を定義できます。 一部のRubyでは、includeメソッドを使用してメソッドをクラスにミックスできます。 しかし、私たちはC ++を使用していますが、これまでのところ可能性は非常に限られています。クラス内でメソッド/属性を直接定義するか、基本クラスからメソッド/属性を継承することができます。







条件に応じてクラス内で異なるメソッド/属性を定義することはできません。 if constexpr



static if



ないif constexpr



C ++。 したがって、継承のみが残ります。







更新しました。 コメントで示唆されているように、私はここでもっと注意深く話すべきです。 C ++にはSFINAEがあるため、SFINAEを介してクラス内の個々のメソッドの可視性を有効/無効にすることができます(つまり、 static if



似た効果を実現しstatic if



)。 しかし、このアプローチには、私の意見では、2つの重大な欠点があります。 まず、そのようなメソッドが1-2-3ではなく、4-5以上の場合、SFINAEを使用して各メソッドを設計するのは面倒であり、これはコードの可読性に影響します。 第二に、SFINAEは、クラス属性(フィールド)の追加/削除を支援しません。

C ++では、 message_holder_t



を継承するいくつかの基本クラスを定義できます。 また、1つまたは別の基本クラスの選択は、テンプレートパラメーターの値に応じて、 std ::条件を使用して既に行われます







しかし、コツは、基本クラスのセットだけでなく、継承の小さなチェーンが必要なことです。 最初に、どのような場合でも必要とされる一般的な機能を決定するクラスがあります。 次は、「スマートポインター」の動作のロジックを決定する基本クラスです。 そして、必要なゲッターを決定するクラスがあります。 この順序で、実装されたクラスを検討します。







SObjectizerには、メッセージがメッセージの可変性をチェックする手段と同様に、message_tから継承されるかどうかを決定する既製のテンプレートマジック既にあるという事実によって、タスクが単純化されています 。 したがって、実装では、この既製の魔法を使用するだけで、その作業の詳細については詳しく説明しません。







共通のポインターストレージベース



対応するintrusive_ptrを格納する共通のベースタイプから始めましょう。また、 message_holder_t



実装のいずれかが必要とするメソッドの共通セットも提供します。







 template< typename Payload, typename Envelope > class basic_message_holder_impl_t { protected : intrusive_ptr_t< Envelope > m_msg; public : using payload_type = Payload; using envelope_type = Envelope; basic_message_holder_impl_t() noexcept = default; basic_message_holder_impl_t( intrusive_ptr_t< Envelope > msg ) noexcept : m_msg{ std::move(msg) } {} void reset() noexcept { m_msg.reset(); } [[nodiscard]] bool empty() const noexcept { return static_cast<bool>( m_msg ); } [[nodiscard]] operator bool() const noexcept { return !this->empty(); } [[nodiscard]] bool operator!() const noexcept { return this->empty(); } };
      
      





このテンプレートクラスには2つのパラメーターがあります。 最初のペイロードは、ゲッターメソッドが使用するタイプを設定します。 一方、2番目のEnvelopeは、intrusive_ptrのタイプを設定します。 メッセージタイプがmessage_t



から継承される場合、これらのパラメーターは両方とも同じ値になります。 ただし、メッセージがmessage_t



から継承されていない場合、メッセージタイプはペイロードとして使用され、 user_type_message_t<Payload>



はEnvelopeとしてuser_type_message_t<Payload>



れます。







基本的にこのクラスの内容は疑問を投げかけないと思います。 ただし、2つの点に注意してください。







まず、ポインター自体、つまり クラス継承者がアクセスできるように、m_msg属性はprotectedセクションで定義されています。







第二に、このクラスでは、コンパイラー自体が必要なすべてのコンストラクターとコピー/移動演算子を生成します。 このクラスのレベルでは、まだ何も禁止していません。







shared_ptrおよびunique_ptrの動作の個別のベース



そのため、メッセージへのポインターを格納するクラスがあります。 これで、相続人を定義できます。これはshared_ptrまたはunique_ptrとして動作します。







shared_ptrの動作の場合から始めましょう。なぜなら、 ここに最小のコードがあります:







 template< typename Payload, typename Envelope > class shared_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() const noexcept { return this->m_msg; } };
      
      





複雑なことはありませんbasic_message_holder_impl_t



から継承し、そのすべてのコンストラクターを継承し、 make_reference()



単純で非破壊的な実装を定義します。







unique_ptrの動作の場合、コードは大きくなりますが、複雑なものはありません。







 template< typename Payload, typename Envelope > class unique_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; unique_message_holder_impl_t( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t( unique_message_holder_impl_t && ) = default; unique_message_holder_impl_t & operator=( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t & operator=( unique_message_holder_impl_t && ) = default; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() noexcept { return { std::move(this->m_msg) }; } };
      
      





繰り返しますが、 basic_message_holder_impl_t



から継承し、必要なコンストラクターを継承します(これはデフォルトのコンストラクターおよび初期化コンストラクターです)。 しかし同時に、unique_ptrロジックに従って、コンストラクターとコピー/移動演算子を定義します。コピーを禁止し、移動を実装します。







ここには破壊的なmake_reference()



メソッドもありmake_reference()









これですべてです。 これらの2つの基本クラスの間の選択を実現するためだけに残ります...







shared_ptrとunique_ptrの動作の選択



shared_ptrの動作とunique_ptrの動作を選択するには、次のメタ関数が必要です(メタ関数はコンパイル時に型で「機能する」ため)。







 template< typename Msg, message_ownership_t Ownership > struct impl_selector { static_assert( !is_signal<Msg>::value, "Signals can't be used with message_holder" ); using P = typename message_payload_type< Msg >::payload_type; using E = typename message_payload_type< Msg >::envelope_type; using type = std::conditional_t< message_ownership_t::autodetected == Ownership, std::conditional_t< message_mutability_t::immutable_message == message_mutability_traits<Msg>::mutability, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> >, std::conditional_t< message_ownership_t::shared == Ownership, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> > >; };
      
      





このメタ関数は、 message_holder_t



パラメーターリストから両方のパラメーターを受け入れ、その結果(つまり、ネストされたtype



定義)、継承元の型を「返します」。 つまり shared_message_holder_impl_t



またはunique_message_holder_impl_t



いずれか。







impl_selector



の定義内では、上記の魔法の痕跡を見ることができますが、ここでは説明しませんでした: message_payload_type<Msg>::payload_type



message_payload_type<Msg>::envelope_type



およびmessage_mutability_traits<Msg>::mutability









そして、メタ関数impl_selector



を使用するimpl_selector



が簡単だったので、短い名前を定義します:







 template< typename Msg, message_ownership_t Ownership > using impl_selector_t = typename impl_selector<Msg, Ownership>::type;
      
      





ゲッターのベース



そのため、すでにポインターを含み、「スマートポインター」の動作を定義するベースを選択する機会があります。 次に、このベースにゲッターメソッドを提供する必要があります。 単純なクラスが1つ必要な理由:







 template< typename Base, typename Return_Type > class msg_accessors_t : public Base { public : using Base::Base; [[nodiscard]] Return_Type * get() const noexcept { return get_ptr( this->m_msg ); } [[nodiscard]] Return_Type & operator * () const noexcept { return *get(); } [[nodiscard]] Return_Type * operator->() const noexcept { return get(); } };
      
      





これは2つのパラメーターに依存するテンプレートクラスですが、それらの意味はまったく異なります。 Baseパラメーターは、上記のimpl_selector



メタ関数の結果です。 つまり Baseパラメーターとして、継承元の基本クラスが設定されます。







コンストラクターとコピー演算子が禁止されているunique_message_holder_impl_t



から継承される場合、コンパイラーはmsg_accessors_t



コンストラクターとコピー演算子を生成できないことに注意することが重要msg_accessors_t



。 これが必要なものです。







メッセージのタイプ、ゲッターによって返されるポインター/リンクは、Return_Typeパラメーターとして機能します。 秘Theは、タイプMsg



不変メッセージのMsg



、Return_Typeパラメーターがconst Msg



設定されることです。 一方、タイプMsg



可変メッセージのMsg



パラメーターReturn_Typeの値はMsg



ます。 したがって、 get()



メソッドは、不変メッセージの場合はconst Msg*



を返し、可変メッセージの場合はMsg*



のみを返します。







無料の関数get_ptr()



を使用すると、 message_t



から継承されていないメッセージを処理get_ptr()



問題message_t



解決されます。







 template< typename M > M * get_ptr( const intrusive_ptr_t<M> & msg ) noexcept { return msg.get(); } template< typename M > M * get_ptr( const intrusive_ptr_t< user_type_message_t<M> > & msg ) noexcept { return std::addressof(msg->m_payload); }
      
      





つまり メッセージがmessage_t



から継承されuser_type_message_t<Msg>



user_type_message_t<Msg>



として保存されている場合、2番目のオーバーロードが呼び出されます。 そして、それが継承される場合、最初のオーバーロード。







ゲッターの特定のベースを選択する



そのため、 msg_accessors_t



テンプレートには2つのパラメーターが必要です。 最初はimpl_selector



メタ関数によって計算されます。 ただし、 msg_accessors_t



から特定の基本型をmsg_accessors_t



するには、2番目のパラメーターの値を決定する必要があります。 もう1つのメタ機能がこれを目的としています。







 template< message_mutability_t Mutability, typename Base > struct accessor_selector { using type = std::conditional_t< message_mutability_t::immutable_message == Mutability, msg_accessors_t<Base, typename Base::payload_type const>, msg_accessors_t<Base, typename Base::payload_type> >; };
      
      





Return_Typeパラメーターの計算にのみ注意を払うことができます。 east constが有用な数少ないケースの1つです;)







さて、後続のコードの可読性を高めるために、それを扱うためのよりコンパクトなオプション:







 template< message_mutability_t Mutability, typename Base > using accessor_selector_t = typename accessor_selector<Mutability, Base>::type;
      
      





最終承継者message_holder_t



これで、これらのすべての基本クラスとメタ関数が必要な実装のために、 message_holder_t



何であるかを見ることができます(message_holderに保存されたメッセージのインスタンスを構築するための実装からいくつかのメソッドが削除されました):







 template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t : public details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> > { using base_type = details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> >; public : using payload_type = typename base_type::payload_type; using envelope_type = typename base_type::envelope_type; using base_type::base_type; friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.message_reference(), b.message_reference() ); } };
      
      





実際、上記の2つのメタ関数の「呼び出し」を記録するには、上記で分析したすべてが必要でした。







 details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> >
      
      





なぜなら これは最初のオプションではありませんが、コードの簡素化と削減の結果、メタ関数のコンパクトなフォームはコードの量を大幅に削減し、その理解度を高めていると言えます(ここで一般的に理解度について話すのが適切な場合)。







そして、どうなるでしょう...



しかし、C ++でif constexpr



がDでstatic if



くらい強力でstatic if



、次のように書くことができます。







constexprのより高度な仮想オプション
 template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t { static constexpr const message_mutability_t Mutability = details::message_mutability_traits<Msg>::mutability; static constexpr const message_ownership_t Actual_Ownership = (message_ownership_t::unique == Ownership || (message_mutability_t::mutable_msg == Mutability && message_ownership_t::autodetected == Ownership)) ? message_ownership_t::unique : message_ownership_t::shared; public : using payload_type = typename message_payload_type< Msg >::payload_type; using envelope_type = typename message_payload_type< Msg >::envelope_type; private : using getter_return_type = std::conditional_t< message_mutability_t::immutable_msg == Mutability, payload_type const, payload_type >; public : message_holder_t() noexcept = default; message_holder_t( intrusive_ptr_t< envelope_type > mf ) noexcept : m_msg{ std::move(mf) } {} if constexpr(message_ownership_t::unique == Actual_Ownership ) { message_holder_t( const message_holder_t & ) = delete; message_holder_t( message_holder_t && ) noexcept = default; message_holder_t & operator=( const message_holder_t & ) = delete; message_holder_t & operator=( message_holder_t && ) noexcept = default; } friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.m_msg, b.m_msg ); } [[nodiscard]] getter_return_type * get() const noexcept { return get_const_ptr( m_msg ); } [[nodiscard]] getter_return_type & operator * () const noexcept { return *get(); } [[nodiscard]] getter_return_type * operator->() const noexcept { return get(); } if constexpr(message_ownership_t::shared == Actual_Ownership) { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() const noexcept { return m_msg; } } else { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() noexcept { return { std::move(m_msg) }; } } private : intrusive_ptr_t< envelope_type > m_msg; };
      
      





私にとって、その違いはあまりにも顕著です。 そして、彼らは現在のC ++を支持していません:(

( C++ "" ).







, , ++. , , , . , message_holder_t



. , , if constexpr



.







おわりに



私の場合、この例はC ++のすべての素晴らしさと貧困を示しています。はい、好きなものを作成できます。ある意味では、テンプレートクラスを作成できます。テンプレートクラスの内容は、テンプレートパラメータに応じて根本的に変化します。







ただし、これを行うには、頭をやや壊し、テンプレートに非常に多くの補助コードを記述する必要があるため、作成者でさえもこのすべてを詳しく調べたくないでしょう。







それにもかかわらず、C ++でこれを行うことができるという事実は、私を個人的に幸せにします。それは、必要な作業量とコード量を混乱させます。しかし、時間の経過とともに、このコードの量とその複雑さが減少することを願っています。原則として、これはすでに見えています。C ++ 98/03の場合、私はそのようなトリックを引き受けることさえしませんが、C ++ 11から始めると、これがより簡単になります。








All Articles