C++ コア ガイドライン:コピーと移動のルール

コピーと移動のルールは非常に明白です。しかし、それらについて説明する前に、コンストラクターの残りの 2 つの規則について書かなければなりません。それらは、コンストラクターの委任と継承に関するものです。

残りの 2 つのルールは次のとおりです。

コンストラクタ ルール

C.51:委譲コンストラクタを使用して共通を表すクラスのすべてのコンストラクターに対するアクション

C++11 以降、コンストラクターはその作業を同じクラスの別のコンストラクターに委譲できます。これは、すべてのコンストラクターに共通のアクションを 1 つのコンストラクターに入れる C++ の最新の方法です。 C++11 より前の C++ コードでは、このようなジョブに init 関数をよく使用していました。

class Degree{
public:
 Degree(int deg){ // (1)
 degree= deg % 360;
 if (degree < 0) degree += 360;
 }

 Degree(): Degree(0){} // (2)

 Degree(double deg): Degree(static_cast<int>(ceil(deg))){} // (3)

private:
 int degree;
};

クラス Degree のコンストラクター (2) および (3) は、すべての初期化作業を、その引数を検証するコンストラクター (1) に委譲します。コンストラクターを再帰的に呼び出すことは、未定義の動作です。

C. 52:継承コンストラクターを使用して、さらに明示的な初期化を必要としない派生クラスにコンストラクターをインポートします

派生クラスで基本クラスのコンストラクターを再利用できる場合は、それを行います。そうしないと、DRY (Don't Repeat Yourself) の原則に違反することになります。

class Rec {
 // ... data and lots of nice constructors ...
};

class Oper : public Rec {
 using Rec::Rec;
 // ... no data members ...
 // ... lots of nice utility functions ...
};

struct Rec2 : public Rec {
 int x;
 using Rec::Rec;
};

Rec2 r {"foo", 7};
int val = r.x; // uninitialized (1) 

継承コンストラクターの使用には危険があります。 Rec2 などの派生クラスに独自のメンバーがある場合、それらは初期化されていません (1)。

コピーと移動

この章はメタルールから始まります。 int のように動作する型とも呼ばれる値型はコピー可能である必要がありますが、クラス階層のインターフェイスはコピー可能ではありません。最後のルール C.67 は、このメタルールを参照しています。

8 つのルールは次のとおりです。

  • C.60:コピー割り当てを virtual 以外にする 、 const& でパラメーターを取得します 、および const& 以外で戻る
  • C.61:コピー操作はコピーする必要があります
  • C.62:コピー割り当てを自己割り当てに対して安全にする
  • C.63:ムーブ割り当てを virtual 以外にする 、 && でパラメータを取ります 、および const& 以外で戻る
  • C.64:移動操作は移動し、そのソースを有効な状態のままにする必要があります
  • C.65:移動割り当てを自己割り当てに対して安全にする
  • C.66:移動操作を noexcept にする
  • C.67:基本クラスはコピーを抑制し、仮想 clone を提供する必要があります 代わりに、「コピー」が必要な場合

コピーと移動の最初の 6 つのルールは、3 つの非常によく似たペアで構成されています。したがって、それらをまとめて説明できます。

  • C.60 および C.63 コピー (移動) 割り当てを非仮想にし、非 const 参照を返す必要があると述べています。方法に違いがあります。パラメータを取る必要があります。
    • コピー代入は、定数左辺値参照 (&) によってパラメータを取得する必要があります 課題のソースを変更してはならないため
    • Move の代入は、 非 const rvalue 参照 (&&) によってパラメータを取る必要があります 課題のソースを変更する必要があるため
    • これは、標準テンプレート ライブラリの代入演算子が従うパターンです。 std::vector を簡単に見てみましょう。
  • C.61C.64 コピー (移動) 操作は実際にはコピー (移動) する必要があります。これは、a =b の予想されるセマンティックです。
  • C.62 および C.65 同じことを述べます。コピー (移動) 割り当ては、自己割り当てに対して安全である必要があります。 x =x は x.
      の値を変更してはなりません。
    • STL、std::string、および int などの組み込み型のコンテナーのコピー (移動) 割り当ては、自己割り当てに対して安全です。したがって、デフォルトで生成されたコピー (移動) 代入演算子は、この場合、自己代入に対して安全です。自己代入に対して安全な型を使用する、自動生成されたコピー (移動) 代入演算子についても同じことが当てはまります。

C.66:Make move オペレーション noexcept

M 以上の操作はスローしないでください。したがって、それらを noexcept として宣言する必要があります。ムーブ コンストラクターと、スローしないムーブ代入演算子を実装できます。

これは、標準テンプレート ライブラリの移動演算子が従うパターンです。 std::vector を見てください。

template<typename T>
class Vector {
 // ...
 Vector(Vector&& a) noexcept :elem{a.elem}, sz{a.sz} { a.sz = 0; a.elem = nullptr; }
 Vector& operator=(Vector&& a) noexcept { elem = a.elem; sz = a.sz; a.sz = 0; a.elem = nullptr; }
 // ...
public:
 T* elem;
 int sz;
};

最後の規則 C.67 はもっと注目に値します。

C.67:基本クラスはコピーを抑制し、仮想 clone を提供する必要があります 代わりに、「コピー」が必要な場合は

このルールの主な理由は、スライスができないことです。スライシングは、C++ におけるこれらの現象の 1 つです。私の同僚はいつも私に警告していました。ウィキペディアには、オブジェクトのスライスに関する記事もあります。

派生クラスのオブジェクトが基本クラスのオブジェクトにコピーされるときに、スライスが発生します。

struct Base { int base_; };
 
struct Derived : Base { int derived_; };
 
int main(){
 Derived d;
 Base b = d; // slicing, only the Base parts of (base_) are copied
}

このシナリオでは、基本クラスのコピー操作が使用されます。したがって、d の基本部分のみがコピーされます。

オブジェクト指向の観点からは、Derived のインスタンスは Base のインスタンスです。つまり、Base のインスタンスが必要なときはいつでも、Derived のインスタンスを使用できます。しかし、注意が必要です。 Base のインスタンスをコピー (値セマンティック) で取得すると、Derived のインスタンスの基本部分のみが取得されます。

void needBase(Base b){ .... };

Derived der;
needBase(der); // slicing kicks in

ガイドラインが提案する解決策は、基本クラスはコピーを抑制し、コピーが必要な場合は代わりに仮想クローン メソッドを提供することです。ガイドラインの例を次に示します。

class B { // GOOD: base class suppresses copying
 B(const B&) = delete;
 B& operator=(const B&) = delete;
 virtual unique_ptr<B> clone() { return /* B object */; }
 // ...
};

class D : public B {
 string more_data; // add a data member
 unique_ptr<B> clone() override { return /* D object */; }
 // ...
};

auto d = make_unique<D>();
auto b = d.clone(); // ok, deep clone

clone メソッドは、新しく作成されたオブジェクトを std::unique_ptr で返します。したがって、所有権は呼び出し元に移ります。このようなクローン メソッドは、ファクトリ メソッドとしてよく知られています。ファクトリ メソッドは、「デザイン パターン:再利用可能なオブジェクト指向ソフトウェアの要素」という本の作成パターンの 1 つです。

次は?

デフォルトの操作にはいくつかのルールが残っています。次の投稿では、比較、スワップ、およびハッシュについて説明します。