C ++でインターフェイスと機能スタイルの実装を分離する

C ++でインターフェイスと機能スタイルの実装を分離する









C ++では、ヘッダーファイルはデータ構造(クラス)の宣言を分離するために使用されます。 これらは、プライベートフィールドを含むクラスの完全な構造を定義します。

この動作の理由は、B。Straustrupの素晴らしい本「C ++の設計と進化」に記載されています。



クラスのプライベートプライベートフィールドを変更するには、クラスの外部インターフェイスのみを使用して、すべての翻訳単位(.cppファイル)を再コンパイルする必要があります。 もちろん、この理由は、インスタンス化中にオブジェクトのサイズを知る必要があることにありますが、問題の原因を知ることは問題自体を解決しません。



この欠点を克服するために、最新のC ++のパワーを使用してみましょう。 猫の下で興味を持ってください。



1.はじめに



まず、上記の論文を再度説明します。 私たちが持っているとしましょう:



-ヘッダーファイル→interface1.h:



class A { public: void next_step(); int result_by_module(int m); private: int _counter; };
      
      





-インターフェースの実装→implementation1.cpp:



 #include "interface1.h" int A::result_by_module(int m) { return _counter % m; } void A::next_step() { ++_counter; }
      
      





-関数main→main.cppのcppファイル:



 #include "interface1.h" int main(int argc, char** argv) { A a; while (argc--) { a.next_step(); } return a.result_by_module(4); }
      
      





クラスAは、プライベート_counterフィールドを持つヘッダーファイルで定義されます。 クラスメソッドのみがこのプライベートフィールドにアクセスできます(ハッキング、友人、およびカプセル化に違反するその他のトリックはお任せください)。



ただし、このフィールドのタイプを変更したい場合は、両方の翻訳単位(implementation.cppファイルとmain.cppファイル)を再コンパイルする必要があります。 メンバ関数はimplementation.cppファイルにあり、タイプAオブジェクトはmain.cppのスタックに作成されます。



C ++をC言語の直接の拡張として考えれば、この状況は理解できます。 マクロアセンブラー:スタック上に作成されるオブジェクトのサイズを知る必要があります。



しかし、一歩前進して、クラス定義を使用してすべての翻訳単位を再コンパイルしないようにしましょう。



2. PIMPLの使用



最初に思い浮かぶのは、PIMPL(実装へのポインター)パターンを使用することです。

ただし、このパターンには欠点があります。すべてのクラスメソッドのラッパーをほぼこの方法で記述する必要がある(メモリ管理の複雑さを省く):



-interface2.h:



 class A_impl; class A { public: A(); ~A(); void next_step(); int result_by_module(int); private: A_impl* _impl; };
      
      





-implementation2.cpp:



 #include "interface2.h" class A_impl { public: A_impl(): _counter(0) {} void next_step() { ++_counter; } int result_by_module(int m) { return _counter % m; } private: int _counter; }; A::A(): _impl(new A_impl) {} A::~A() { delete _impl; } int A::result_by_module(int m) { return _impl->result_by_module(m); } void A::next_step() { _impl->next_step(); }
      
      





3. std :: functionに外部インターフェイスを作成します



このパターンを「より機能的に」し、パブリックインターフェイスからクラスの内部デバイスを解放してみましょう。



外部インターフェイスの場合、メソッドを格納するstd :: function型のフィールドを持つ構造体を使用します。 また、「仮想コンストラクター」-スマートポインターにラップされた新しいオブジェクトを返す無料の関数も定義します。



-interface3.h:



 struct A { std::function<int(int)> _result_by_module; std::function<void()> _next_couter; }; std::unique_ptr<A> create_A();
      
      





完全に「ガルバニック」なアンタイドクラスインターフェイスを取得しました。 実装について考える時間です。



実装は、無料の機能、つまり仮想コンストラクターで始まります。



 std::unique_ptr<A> create_A(int start_i) { std::unique_ptr<A> result(new A()); result->result_by_module_ = ??? result->next_counter_ = ??? return result; }
      
      





オブジェクトAの内部状態をどのように保存しますか? これを行うには、外部オブジェクトの内部状態を記述する別のクラスを作成しますが、外部オブジェクトとは一切接続しません。



 struct A_context { int counter_; };
      
      





したがって、状態を格納するオブジェクトのタイプがあり、このタイプは外部インターフェイスとは関係ありません!



また、メソッドの役割を果たす、無料の静的関数__A_result_by_moduleを作成します。 この関数は、A_context型のオブジェクトを最初の引数として受け入れます(より正確には、スマートポインター。Pythonのように見えませんか?)。 スコープを狭めるために、関数を匿名の名前空間に配置します。



 namespace { static int __A_result_by_module(std::shared_ptr<A_context> ctx, int m) { return ctx->counter_ % m; } }
      
      





create_A関数に戻ります。 std :: bind関数を使用して、C_contextオブジェクトと__A_result_by_module関数を1つの全体にバインドします。



多様性のために、新しい関数を使用せずにラムダ関数を使用してnext_counterメソッドを実装します。



 std::unique_ptr<A> create_A() { std::unique_ptr<A> result(new A()); auto ctx = std::make_shared<A_context>(); //   -    ctx->counter_ = 0; //   result->_result_by_module = std::bind( __A_result_by_module, ctx, std::placeholders::_1); result->_next_step = [ctx] () -> void { ctx->counter_++; }; return result; }
      
      





4.最後の例



合計で、記事の最初からのコードは、次の画像で書き直すことができます。



-interface.h:



 #include <functional> #include <memory> struct A { std::function<int(int)> _result_by_module; std::function<void()> _next_step; }; std::unique_ptr<A> create_A();
      
      





-implementation.cpp:



 #include "interface3.h" #include <memory> struct A_context { int counter_; }; namespace { static int __A_result_by_module(std::shared_ptr<A_context> ctx, int i) { return ctx->counter_ % i; } } std::unique_ptr<A> create_A() { std::unique_ptr<A> result(new A()); auto ctx = std::make_shared<A_context>(); ctx->counter_ = 0; result->_result_by_module = std::bind( __A_result_by_module, ctx, std::placeholders::_1); result->_next_step = [ctx] () -> void { ctx->counter_++; }; return result; }
      
      





-main.cpp:



 #include "interface3.h" int main(int argc, char** argv) { auto a = create_A(); while (argc--) { a->_next_step(); } return a->_result_by_module(4); }
      
      





4.1。 メモリの所有と管理について少し



オブジェクトの所有権スキームは、次のように説明できます。外部インターフェイスオブジェクトは、「メソッド」のファンクターを所有します。 「メソッド」のファンクターは、内部状態の1つのオブジェクトを共同で所有します。



したがって、外部インターフェイスオブジェクトのライフタイムは、内部状態オブジェクトとファンクタオブジェクトのリリースの時間を決定します。 外部インターフェイスオブジェクトが解放されると、ファンクターオブジェクトが解放されます。 内部状態のオブジェクトはファンクターオブジェクトによってのみ所有されているため、最後のファンクターオブジェクトが解放された時点で、内部状態のオブジェクトが解放されます。



5.まとめ



したがって、オブジェクトの内部状態を外部インターフェイスから切り離すことができました。 明示的に分割:



1.外部インターフェース:

-内部状態に依存しないstd ::関数に基づくインターフェイスが使用されます



2.オブジェクトを生成するためのメカニズム:

-無料の機能が使用されます。 これにより、生成パターンを簡単に実装できます。



3.オブジェクト内部状態

-オブジェクトの内部状態を記述する別のクラスが使用され、そのスコープは完全に1つの変換単位(cppファイル)内にあります。



4.内部状態と外部インターフェイスのリンク

-小さなメソッド/ゲッター/セッター/にラムダ関数を使用...

-std :: bind関数と、自明でないロジックを持つメソッドのfree関数が使用されます。



さらに、このコードのフレームワーク内でのコードのテスト容易性はより高くなります。これは、メソッドが単なるフリー関数であるため、メソッドの単体テストを作成することが容易になったためです。



All Articles