C++ コア ガイドライン:0、5、または 6 の規則

この投稿は、0、5、または 6 のルールに関するものです。また、コピー セマンティックと参照セマンティックの違いと、非常によく似たトピックであるディープ コピーとシャロー コピーについても説明します。

正確には、C++ には、オブジェクトのライフサイクルを管理するための約 50 の規則があります。今回は非常に重要な3つのデフォルト運用ルールについて書いていきます。 C++ コア ガイドラインの各ルールへのリンクを提供します。必要に応じて、リンクに従って詳細を読むことができます。始めましょう。

C++ には、オブジェクトのライフサイクルを管理するための 6 つの既定の操作 (特殊関数とも呼ばれる) が用意されています。したがって、オブジェクトのライフサイクルへのこの最初の投稿は、6 つの操作から開始する必要があります。

  • デフォルトのコンストラクタ:X()
  • コピー コンストラクタ:X(const X&)
  • コピーの割り当て:operator=(const X&)
  • 移動コンストラクタ:X(X&&)
  • 移動の割り当て:operator=(X&&)
  • デストラクタ:~X()

デフォルトの操作は関連しています。つまり、そのうちの 1 つを実装または削除すると、残りの 5 つについて考える必要があります。実装という言葉は少し混乱するかもしれません。デフォルトのコンストラクターの場合、それを定義したり、コンパイラーから要求したりできることを意味します:

X(){}; // explicitly defined
X() = default; // requested from the compiler

この規則は、他の 5 つのデフォルト操作にも当てはまります。

デフォルトの操作ルールのセットについて書く前に、1 つ一般的な注意事項があります。 C++ は、その型に対して参照セマンティックではなく、値セマンティックを提供します。 https://isocpp.org/wiki/faq/value-vs-ref-semantics から両方の用語について見つけた最良の定義を次に示します。

  • 値のセマンティック :値 (または「コピー」) のセマンティクスは、代入がポインターだけでなく値をコピーすることを意味します。
  • 参照セマンティック: 参照セマンティクスでは、代入はポインタ コピーです (つまり、参照 ).

最初の 3 つのルールは次のとおりです。

一連のデフォルト操作ルール:

  • C.20:デフォルト オペレーションの定義を避けることができる場合は、そうします
  • C.21:または =delete を定義する場合 任意のデフォルト操作、define または =delete それらすべて
  • C.22:デフォルトの操作に一貫性を持たせる

C.20:デフォルト オペレーションの定義を回避できる場合は、実行してください

このルールは「ゼロのルール」とも呼ばれます "。つまり、すべてのメンバーが 6 つの特別な関数を持っているため、クラスにデフォルト操作が必要ない場合は、完了です。

struct Named_map {
public:
 // ... no default operations declared ...
private:
 string name;
 map<int, int> rep;
};

Named_map nm; // default construct
Named_map nm2 {nm}; // copy construct

デフォルトの構築とコピーの構築は、std::string と std::map に対して既に定義されているため、機能します。

C.21:定義または =delete 任意のデフォルト操作、define または =delete それらすべて

6 つすべてを定義または削除する必要があるため、このルールは「5 のルール」と呼ばれます。 ". 5 は私には奇妙に思えます。5 または 6 のルールの理由は非常に明白です。6 つの操作は密接に関連しています。したがって、ルールに従わない場合、非常に奇妙なオブジェクトが得られる可能性が非常に高くなります。 . ガイドラインの例を次に示します。

struct M2 { // bad: incomplete set of default operations
public:
 // ...
 // ... no copy or move operations ...
 ~M2() { delete[] rep; }
private:
 pair<int, int>* rep; // zero-terminated set of pairs
};

void use()
{
 M2 x;
 M2 y;
 // ...
 x = y; // the default assignment
 // ...
}

この例のどこがおかしいのでしょうか?まず、デストラクタは、初期化されていない rep を削除します。第二に、それはより深刻です。最後の行のデフォルトのコピー割り当て操作 (x =y) は、M2 のすべてのメンバーをコピーします。これは特に、ポインタ担当者がコピーされることを意味します。したがって、x と y のデストラクタが呼び出され、二重削除のために未定義の動作が発生します。

C.22:デフォルト操作の一貫性を保つ

このルールは、前のルールに関連しています。デフォルトの操作を異なるセマンティクスで実装すると、クラスのユーザーが非常に混乱する可能性があります。これが理由で、クラス Strange を構築しました。奇妙な動作を観察するために、Strange には int へのポインターが含まれています。

// strange.cpp (https://github.com/RainerGrimm/ModernesCppSource)

#include <iostream> struct Strange{ Strange(): p(new int(2011)){} // deep copy Strange(const Strange& a) : p(new int(*(a.p))){} // (1) // shallow copy Strange& operator=(const Strange& a){ // (2) p = a.p; return *this; } int* p; }; int main(){ std::cout << std::endl; std::cout << "Deep copy" << std::endl; Strange s1; Strange s2(s1); // (3) std::cout << "s1.p: " << s1.p << "; *(s1.p): " << *(s1.p) << std::endl; std::cout << "s2.p: " << s2.p << "; *(s2.p): " << *(s2.p) << std::endl; std::cout << "*(s2.p) = 2017" << std::endl; *(s2.p) = 2017; // (4) std::cout << "s1.p: " << s1.p << "; *(s1.p): " << *(s1.p) << std::endl; std::cout << "s2.p: " << s2.p << "; *(s2.p): " << *(s2.p) << std::endl; std::cout << std::endl; std::cout << "Shallow copy" << std::endl; Strange s3; s3 = s1; // (5) std::cout << "s1.p: " << s1.p << "; *(s1.p): " << *(s1.p) << std::endl; std::cout << "s3.p: " << s3.p << "; *(s3.p): " << *(s3.p) << std::endl; std::cout << "*(s3.p) = 2017" << std::endl; *(s3.p) = 2017; // (6) std::cout << "s1.p: " << s1.p << "; *(s1.p): " << *(s1.p) << std::endl; std::cout << "s3.p: " << s3.p << "; *(s3.p): " << *(s3.p) << std::endl; std::cout << std::endl; std::cout << "delete s1.p" << std::endl; delete s1.p; // (7) std::cout << "s2.p: " << s2.p << "; *(s2.p): " << *(s2.p) << std::endl; std::cout << "s3.p: " << s3.p << "; *(s3.p): " << *(s3.p) << std::endl; std::cout << std::endl; }

クラス Strange には、コピー コンストラクター (1) とコピー代入演算子 (2) があります。コピー コンストラクターは、ディープ コピーと代入演算子の浅いコピーを使用します。ほとんどの場合、型にディープ コピー セマンティック (値セマンティック) が必要ですが、これら 2 つの関連する操作に異なるセマンティックを使用することはおそらくありません。

違いは、ディープ コピー セマンティックは 2 つの分離された新しいオブジェクト (p(new int(*(a.p))) を作成するのに対し、浅いコピー セマンティックは単にポインター (p =a.p) をコピーすることです。Strange 型で遊んでみましょう。プログラム。

式 (3) では、コピー コンストラクターを使用して s2 を作成しています。ポインタのアドレスを表示し、ポインタの値を変更する s2.p (4) が示すように、s1 と s2 は 2 つの別個のオブジェクトです。それは s1 と s3 には当てはまりません。式 (5) のコピー代入は、浅いコピーをトリガーします。その結果、ポインター s3.p (6) を変更すると、ポインター s1.p にも影響が及びます。したがって、両方のポインターの値は同じです。

ポインター s1.p (7) を削除すると、楽しみが始まります。ディープ コピーのおかげで、s2.p に悪いことは何も起こりませんでした。ただし、値は s3.p ヌル ポインターになります。より正確に言うと、(*s3.p) のような null ポインターの逆参照は未定義の動作です。

次のステップ

オブジェクトのライフサイクルに対する C++ コア ガイドラインの話は続きます。それはオブジェクトの破壊のための規則に続きます。これは、次の投稿の計画でもあります。