コピーペーストの代わりにC ++でポリシーベースのデザインを使用し、OOP階層を作成する例

C ++は、不当な複雑さのために非難されることが非常に多い。 もちろん、C ++言語は複雑です。 そして、それぞれの新しい標準で、それはますます難しくなっています。 しかし、パラドックスは、C ++が常に複雑になり、C ++が順次かつ漸進的に開発者の生活を簡素化することです。 BoostやFollyの開発者よりも簡単なコードを書く通常のプログラマを含みます。 根拠にならないように、「最近のものから」の小さな例でこれを示してみます。さまざまな条件への適応の結果として、些細なクラスがポリシーベースの設計を使用して軽いハードコアになった方法。



そのため、特定のクラスのセットを変更して、プロセスに費やした時間に関する統計情報のコレクションを追加するタスクが発生しました。 それほど多くのクラスはありませんが、12個ほどありますが、一部のクラスはロジックが単純ではありません。 外部では同じインターフェースを公開しますが、内部ではそれぞれ独自の方法で動作しますが、もちろん、それぞれの実装で同様の要素がいくつか見つかります。



このタスクを実装する過程で、変更可能な各クラスがそのようなプライベートメソッドのセットを取得することがすぐに明らかになりました。



class some_performer_t { ... void work_started() { std::lock_guard< activity_tracking::lock_t > lock{ m_stats_lock }; m_is_in_working = true; m_work_started_at = activity_tracking::clock_type_t::now(); m_work_activity.m_count += 1; } void work_finished() { std::lock_guard< activity_tracking::lock_t > lock{ m_stats_lock }; m_is_in_working = false; activity_tracking::update_stats_from_current_time( m_work_activity, m_work_started_at ); } activity_tracking::stats_t take_work_stats() { activity_tracking::stats_t result; bool is_in_working{ false }; activity_tracking::clock_type_t::time_point work_started_at; { std::lock_guard< activity_tracking::lock_t > lock{ m_stats_lock }; result = m_work_activity; if( true == (is_in_working = m_is_in_working) ) work_started_at = m_work_started_at; } if( is_in_working ) activity_tracking::update_stats_from_current_time( result, work_started_at ); return result; } ... activity_tracking::lock_t m_stats_lock; bool m_is_in_working; activity_tracking::clock_type_t::time_point m_work_started_at; activity_tracking::stats_t m_work_activity; ... };
      
      





一部のクラスでは、work_started()/ work_finished()/ take_work_stats()の代わりに、wait_started()/ wait_finished()/ take_wait_stats()メソッドがあります。 そして、いくつかと、それらと他の両方。 ただし、これらのメソッド内のコードは実質的に1-in-1に一致します。



同じものを複製したくなかったのは明らかなので、すべての詳細はstats_collector_t補助クラスに移動され、その後メインコードは次のようになり始めました。



 class some_performer_t { ... void work_started() { m_work_stats.start(); } void work_finished() { m_work_stats.stop(); } activity_tracking::stats_t take_work_stats() { return m_work_stats.take_stats(); } ... activity_tracking::stats_collector_t m_work_stats; ... };
      
      





stats_collector_tクラスは、最初は非常にシンプルに見えました。



 class stats_collector_t { public : void start() { /*    work_started */ } void stop() { /*    work_finished */ } stats_t take_stats() { /*    take_work_stats */ } private : lock_t m_lock; bool m_is_in_working{ false }; clock_type_t::time_point m_work_started_at; stats_t m_work_activity{}; };
      
      





すべてが良いようです。 しかし、最初の待ち伏せが発見されました。場合によっては、stats_collector_tが独自のロックを持たないようにする必要があります。 たとえば、一部のクラスパフォーマンスでは、stats_collector_tのインスタンスがいくつかあり、各stats_collector_tは異なる種類の作業の統計をカウントしますが、それらの作業は同じロックの下で実行されます。 つまり 一部の場所ではstats_collector_tが独自のロックを持ち、他の場所では他の誰かのロックを使用できる必要があることが判明しました。



まあ、問題ありません。 stats_collector_tをテンプレートに変換します。テンプレートのパラメーターは、内部ロックオブジェクトを使用するか外部ロックオブジェクトを使用するかを示します。



 template< LOCK_HOLDER > class stats_collector_t { public : //     ,    // -    LOCK_HOLDER-. //            // LOCK_HOLDER,    stats_collector_t. template< typename... ARGS > stats_collector_t( ARGS && ...args ) : m_lock_holder{ std::forward<ARGS>(args)... } {} void start() { std::lock_guard< LOCK_HOLDER > lock{ m_lock_holder }; ... /*      */ } void stop() { std::lock_guard< LOCK_HOLDER > lock{ m_lock_holder }; ... /*      */ } stats_t take_stats() {...} private : LOCK_HOLDER m_lock_holder; bool m_is_in_working{ false }; clock_type_t::time_point m_work_started_at; stats_t m_work_activity{}; };
      
      





次のクラスをLOCK_HOLDERとして使用する必要がありました。



 class internal_lock_t { lock_t m_lock; public : internal_lock_t() {} void lock() { m_lock.lock(); } void unlock() { m_lock.unlock(); } }; class external_lock_t { lock_t & m_lock; public : external_lock_t( lock_t & lock ) : m_lock( lock ) {} void lock() { m_lock.lock(); } void unlock() { m_lock.unlock(); } };
      
      





したがって、パフォーマークラスでは、stats_collector_tのインスタンスが次の2つの方法のいずれかで初期化され始めました。



 using namespace activity_tracking; class one_performer_t { ... private : //  ,     lock-. lock_t m_common_lock; stats_collector_t< external_lock_t > m_work_stats{ m_common_lock }; stats_collector_t< external_lock_t > m_wait_stats{ m_common_lock }; ... }; class another_performer_t { ... private : //  ,     lock-. stats_collector_t< internal_lock_t > m_work_stats{}; ... };
      
      





確かに、待ち伏せもここで明らかにされました。 外部ロックオブジェクトのタイプが常にactivity_tracking :: lock_tになるとは限らないことが判明しました。 別の種類のロックオブジェクトを使用する必要がある場合がありますが、std :: lock_guardでの作業に適しています。



したがって、補助クラスexternal_lock_tもテンプレートになりました。



 template< typename LOCK = lock_t > class external_lock_t { LOCK & m_lock; public : external_lock_t( LOCK & lock ) : m_lock( lock ) {} void lock() { m_lock.lock(); } void unlock() { m_lock.unlock(); } };
      
      





その結果、stats_collector_tの使用は次のようになりました。



 using namespace activity_tracking; class one_performer_t { ... private : //  ,     lock-. lock_t m_common_lock; stats_collector_t< external_lock_t<> > m_work_stats{ m_common_lock }; stats_collector_t< external_lock_t<> > m_wait_stats{ m_common_lock }; ... }; class tricky_performer_t { ... private : //  ,     lock- // -  . mpmc_queue_traits::lock_t m_common_lock; stats_collector_t< external_lock_t< mpmc_queue_traits::lock_t > > m_work_stats{ m_common_lock }; stats_collector_t< external_lock_t< mpmc_queue_traits::lock_t > > m_wait_stats{ m_common_lock }; ... };
      
      





しかし、結局のところ、これらはまだ花でした。 ベリーは、いくつかのケースでstart()およびstop()メソッドでロックオブジェクトをキャプチャすることが不可能であることが判明したときに行きました。 これらのメソッドは、外部ロックオブジェクトが既にキャプチャされているコンテキストで呼び出されます。



最初に考えたのは、start_no_lock()/ start()およびstop_no_lock()/ stop()メソッドのペアを作成することでした。 しかし、これはまあまあのアイデアです。 特に、このような分割により、一部のテンプレートでのstats_collectorの使用が複雑になる可能性があります。 テンプレートコードでは、start_no_lock()を呼び出すのか、単にstart()を呼び出すのかが明確でない場合があります。 とにかく、start_)とstart_no_lock()の存在は見苦しく、stats_collectorの使用を複雑にします。



そのため、stats_collector_tテンプレートの動作が変更されました。



 template< typename LOCK_HOLDER > class stats_collector_t { using start_stop_lock_t = typename LOCK_HOLDER::start_stop_lock_t; using take_stats_lock_t = typename LOCK_HOLDER::take_stats_lock_t; public : ... void start() { start_stop_lock_t lock{ m_lock_holder }; ... } void stop() { start_stop_lock_t lock{ m_lock_holder }; ... } stats_t take_stats() { ... { take_stats_lock_t lock{ m_lock_holder }; ... } ... } ... };
      
      





LOCK_HOLDER型は、start_stop_lock_t(start()およびstop()メソッドでのロックの実行方法)およびtake_stats_lock_t(take_stats()メソッドでのロックの実行方法)の2つのタイプ名を定義する必要があります。 そして、すでにクラスstats_collector_tが、その助けを借りて、コード内のロックオブジェクトをロックします。



単純なクラスinternal_lock_tは、これらの名前を簡単な方法で定義します。



 class internal_lock_t { lock_t m_lock; public : using start_stop_lock_t = std::lock_guard< internal_lock_t >; using take_stats_lock_t = std::lock_guard< internal_lock_t >; internal_lock_t() {} void lock() { m_lock.lock(); } void unlock() { m_lock.unlock(); } };
      
      





ただし、external_lock_tテンプレートを展開し、もう1つのパラメーター(ブロッキングポリシー)を追加する必要があります。



 template< typename LOCK_TYPE = lock_t, template<class> class LOCK_POLICY = default_lock_policy_t > class external_lock_t { LOCK_TYPE & m_lock; public : using start_stop_lock_t = typename LOCK_POLICY< external_lock_t >::start_stop_lock_t; using take_stats_lock_t = typename LOCK_POLICY< external_lock_t >::take_stats_lock_t; external_lock_t( LOCK_TYPE & lock ) : m_lock( lock ) {} void lock() { m_lock.lock(); } void unlock() { m_lock.unlock(); } };
      
      





さて、ブロックポリシーのクラスの実装は次のようになります。



 template< typename L > struct no_actual_lock_t { no_actual_lock_t( L & ) {} /*     */ }; template< typename LOCK_HOLDER > struct default_lock_policy_t { using start_stop_lock_t = std::lock_guard< LOCK_HOLDER >; using take_stats_lock_t = std::lock_guard< LOCK_HOLDER >; }; template< typename LOCK_HOLDER > struct no_lock_at_start_stop_policy_t { using start_stop_lock_t = no_actual_lock_t< LOCK_HOLDER >; using take_stats_lock_t = std::lock_guard< LOCK_HOLDER >; }
      
      





default_lock_policy_tの場合、std :: lock_guardクラスはstart_stop_lock_tとして機能し、start()/ stop()メソッドにはロックオブジェクトの実際のロックがあることがわかります。 ただし、no_lock_at_start_stop_policy_tポリシーが使用される場合、start_stop_lock_tは空のno_actual_lock_t型であり、コンストラクターでもデストラクターでも何も行いません。 そのため、start()/ stop()にはロックがありません。 はい。また、start_stop_lock_tインスタンス自体(別名no_actual_lock_t)は、おそらく最適化コンパイラーによって破棄されます。



さて、さまざまなケースでのstats_collector_tの使用は、次のようになり始めました。



 using namespace activity_tracking; class one_performer_t { ... private : //  ,     lock-. lock_t m_common_lock; stats_collector_t< external_lock_t<> > m_work_stats{ m_common_lock }; stats_collector_t< external_lock_t<> > m_wait_stats{ m_common_lock }; ... }; class tricky_performer_t { ... private : //  ,     lock- // -  . mpmc_queue_traits::lock_t m_common_lock; stats_collector_t< external_lock_t< mpmc_queue_traits::lock_t > > m_work_stats{ m_common_lock }; stats_collector_t< external_lock_t< mpmc_queue_traits::lock_t > > m_wait_stats{ m_common_lock }; ... }; class very_tricky_performer_t { ... private : //  ,     lock- // -  ,        // start()  stop()  . complex_task_queue_t::lock_t m_common_lock; stats_collector_t< external_lock_t< complex_task_queue_t::lock_t, no_lock_at_start_stop_policy_t > > m_wait_stats{ m_common_lock }; ... };
      
      





同時に、プリフォーマークラスでは、stats_collectorオブジェクトに対して同じstart()/ stop()/ take_stats()メソッドを呼び出し、それらを呼び出し続けました。 この点で、パフォーマーにとっては何も変わっていません。動作のすべての違いは、対応するstats_collectorオブジェクトが宣言されたときに明示的に示されます。 つまり 実行時にオーバーヘッドを追加することなく、コンパイル時に特定のstats_collectorの動作を取得しました。



代替案は何でしょうか? おそらく、stats_collectorsのいくつかのオプションを記述でき、start()/ stop()の動作の詳細は異なりますが、基本的には互いに重複しています。 または、stats_collectorを特定の実装がオーバーライドする抽象クラス(インターフェース)にして、start()/ stop()メソッドの動作をオーバーライドすることもできます。 最終的には短くて簡単だとは思わないでください。 むしろその逆です。 したがって、この場合のポリシーベースの設計の使用は非常に適切に見えます。



ストーリー全体のモラルは何ですか? C ++言語は複雑ですが、正当化された複雑さです。 テンプレートなしのC ++の方がはるかに簡単でした。 しかし、その上でのプログラミングはより困難でした。



この例で使用されているポリシーベースの設計のように、パターンが現れ、新しいアプローチが利用可能になりました。 これにより、コードの有効性を損なうことなくコードの再利用が簡素化されました。 つまり プログラマーにとって簡単になりました。



次に、可変個引数テンプレートが登場しました。 もちろん、それは言語をさらに難しくしました。 しかし、プログラミングはさらに簡単になりました。 stats_collector_tクラスのコンストラクターをご覧ください。 これは1つであり、理解しやすいものです。 変数を使用しないと、異なる数の引数に対して複数のコンストラクターをハードコーディングする必要があります(またはマクロに頼る)。



さて、それは朗報です。C++開発プロセスは継続しています。 将来、この言語をさらに使いやすくするものは何でしょうか。 もちろん、それまでに他の誰かがそれを使い続けない限り...)



All Articles