ゼロのルール

c ++ 03に関しては、「3つの規則」があり、c ++ 11の出現により「5の規則」に変換されました。 これらのルールは、基本的に独自のデータ型を設計するための非公式の推奨事項にすぎませんが、それでも多くの場合便利です。 ゼロの規則は、これらの推奨事項を継続しています。 この投稿では、実際には最初の2つのルールを思い出し、「ゼロルール」の背後にある考え方を説明しようとします。



やる気



上記のすべてのルールは、クラスのオブジェクトがリソース(ハンドラー、リソースへのポインター)を所有し、コピー時にこのハンドラーとリソース自体に何が起こるかを何らかの形で決定する必要がある場合に、主に(常にではありませんが)記述されます/オブジェクトを移動します。

デフォルトでは、「特別な」関数(コピーコンストラクタ、代入演算子、デストラクタなど)を宣言しない場合、コンパイラはコードを自動的に生成します。 同時に、期待どおりに動作します。 たとえば、コピーコンストラクターは、対応するコピーコンストラクターを呼び出して非PODクラスメンバーをコピーし、 PODタイプのメンバーをビット単位でコピーしようとします。 この振る舞いは、それらのすべてのメンバーを含む単純なクラスにはまったく受け入れられます。



所有戦略



大規模で複雑なクラス、または外部リソースのハンドラーがメンバーとして機能するクラスの場合、デフォルトのコンパイラーによって実装された動作はもはや私たちに合わないかもしれません。 幸いなことに、特定の状況で必要なリソース所有戦略を実装することで、特別な機能を独自に決定できます。 従来、このような戦略はいくつかあります。

1.コピーおよび移動の禁止。

2.ハンドラとともに共有リソースをコピーしますディープコピー )。

3.コピーは禁止されていますが、移動は許可されています。

4.共有(参照カウントなどにより規制されている)。



「3つのルール」と「5つのルール」



したがって、「3つのルール」と「5つのルール」は、一般的な場合、選択された戦略の1つに従ってオブジェクトをコピー、移動、または破棄する操作の1つを独立して決定する必要がある場合、おそらく正しく動作する必要があると言います他のすべての機能も決定します。

これがなぜそうなのかは、次の例で簡単にわかります。 クラスのメンバーがヒープ上のオブジェクトへのポインターだとしましょう。



class my_handler { public: my_handler(int c) : counter_(new int(c)) {} private: int* counter_; };
      
      







この状況でのデフォルトのデストラクタは、counter_ポインタ自体のみを破壊し、ポイントするものを破壊しないため、私たちには適していません。 デストラクタを定義します。



 my_handler::~my_handler() {delete counter_;}
      
      







しかし、クラスのオブジェクトをコピーしようとするとどうなりますか? デフォルトのコピーコンストラクターが呼び出されます。このコンストラクターは、ポインターを正直にコピーします。その結果、同じリソースへのポインターを所有する2つのオブジェクトが作成されます。 これは明らかな理由で悪いです。 したがって、独自のコピーコンストラクタ、代入演算子などを定義する必要があります。

それで、取引は何ですか? 5つの「特別な」関数すべてを常に定義しましょう。すべてが大丈夫です。 可能ですが、率直に言って、非常に疲れており、エラーに満ちています。 次に、現在の状況で本当に必要なもののみを決定し、残りをコンパイラーで生成させますか? これはオプションでもありますが、まず、コードが使用される「状況」は知識なしに変更される可能性があり、クラスは新しい条件で動作できなくなります。次に、抑制する特別なルールがあります。コンパイラ生成仕様。 機能。 たとえば、「5kから明示的に宣言された関数が少なくとも1つある場合、コンパイラは移動関数を暗黙的に生成しません」または「明示的に宣言された移動関数が少なくとも1つある場合、コピー関数は生成されません」。



「ゼロの法則」



可能な解決策の1つは、マルティノフェルナンデスによって「ゼロルール」の形式で表明され、次のように要約することができます。 そして、そのような特別なクラスはすでに標準ライブラリにあります。 これらはstd :: unique_ptrおよびstd :: shared_ptrです。 これらのクラスを使用する場合、カスタム削除機能を設定できるという事実により、それらを使用して上記の所有権戦略のほとんどを実装できます(少なくとも最も有用です)。 たとえば、共有所有権が意味をなさない、または有害なオブジェクト(ファイル記述子、ミューテックス、ストリームなど)をクラスが所有している場合、このオブジェクトを対応する削除者でstd :: unique_ptrでラップします。 これで、クラスのオブジェクトはコピー(移動のみ)できなくなり、オブジェクトが破棄されるときにリソースが正しく破棄されることが自動的に保証されます。 ストアドハンドラのセマンティクスがリソースの共有を許可している場合、 shared_ptrを使用します。 例として、カウンターへのポインターを使用した上記の例が適しています。

待ってください...しかし、ポリモーフィックな継承の状況では、派生オブジェクトが正しく破棄されるように仮想デストラクタを宣言する必要があります。 ここで「ゼロルール」は適用されないことがわかりますか? あまり好きではありません。 Shared_ptrは、このような状況で役立ちます。 事実、 deleter shared_ptrは、 そこに格納されているポインターの実際のタイプを「記憶」しています。



 struct base {virtual void foo() = 0;}; struct derived : base {void foo() override {...}}; base* bad = new derived; delete bad; // !     base { ... std::shared_ptr<base> good = std::make_shared<derived>(); } // ! shared_ptr     .
      
      







shared_ptrのオーバーヘッドに混乱した場合、またはポリモーフィックオブジェクトへのポインターの排他的所有権を提供したい場合は、unique_ptrでラップすることもできますが、独自のカスタム削除機能を記述する必要があります。



 typedef std::unique_ptr<base, void(*)(void*)> base_ptr; base_ptr good{new base, [](void* p){delete static_cast<derived*>(p);}};
      
      







後者の方法には、特定の問題が伴います。 多重継承の場合、2つ(またはそれ以上)の異なる削除プログラムを作成する必要があります。削除プログラムの実装が異なる可能性があるにもかかわらず、1つのスマートポインターを別のスマートポインターから移動することもできます。



そのため、「ゼロルール」はリソース管理メカニズムへの別のアプローチですが、他のC ++イディオムと同様に、それを考えずに使用することはできません。 それぞれの特定の状況で、それを適用することが理にかなっているかどうかを個別に決定する必要があります。 以下のリンクには、このトピックに関するScott Meyersによる記事があります。



参照資料


flamingdangerzone.com/cxx11/2012/08/15/rule-of-zero.html

scottmeyers.blogspot.ru/2014/03/a-concern-about-rule-of-zero.html

stackoverflow.com/questions/4172722/what-is-the-rule-of-three

stackoverflow.com/questions/4782757/rule-of-three-becomes-rule-of-five-with-c11



All Articles