サーチ…
ファイブのルール
C ++ 11では、moveコンストラクタとmove代入演算子の2つの新しい特別なメンバ関数が導入されています。 C ++ 03でRule of Threeを守るのと同じ理由で、通常はC ++ 11のRule of Fiveに従いたいと思う:クラスが5つの特別なメンバー関数のうちの1つを必要とし、それらのうちのすべてを必要とする可能性が最も高い。
ただし、Rule of Fiveに従わないことは、通常、エラーとはみなされませんが、3つのルールが引き続き適用される限り、最適化の機会を逃すことになります。コンパイラーが通常使用するときに移動コンストラクターまたは移動代入演算子が使用できない場合は、可能であればコピーセマンティクスを使用し、不必要なコピー操作のために操作が効率が低下します。移動セマンティクスがクラスに対して望ましくない場合、移動コンストラクタまたは代入演算子を宣言する必要はありません。
3つのルールの場合と同じ例:
class Person
{
char* name;
int age;
public:
// Destructor
~Person() { delete [] name; }
// Implement Copy Semantics
Person(Person const& other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
Person &operator=(Person const& other)
{
// Use copy and swap idiom to implement assignment.
Person copy(other);
swap(*this, copy);
return *this;
}
// Implement Move Semantics
// Note: It is usually best to mark move operators as noexcept
// This allows certain optimizations in the standard library
// when the class is used in a container.
Person(Person&& that) noexcept
: name(nullptr) // Set the state so we know it is undefined
, age(0)
{
swap(*this, that);
}
Person& operator=(Person&& that) noexcept
{
swap(*this, that);
return *this;
}
friend void swap(Person& lhs, Person& rhs) noexcept
{
std::swap(lhs.name, rhs.name);
std::swap(lhs.age, rhs.age);
}
};
代わりに、コピーと移動の代入演算子の両方を、コピー・アンド・スワップ・イディオムの使用を容易にするために参照または参照値の代わりに値でインスタンスを取る単一の代入演算子で置き換えることもできます。
Person& operator=(Person copy)
{
swap(*this, copy);
return *this;
}
3つのルールから5つのルールへの拡張はパフォーマンスの理由から重要ですが、ほとんどの場合、厳密には必要ではありません。コピーコンストラクタと代入演算子を追加すると、型を移動してもメモリがリークすることはありません(移動構成はその場合は単にコピーに戻ります)が、呼び出し元が予期しなかったコピーを実行することになります。
ルールゼロ
私たちは、5つのルールとRAIIの原則を組み合わせることで、より希薄なインターフェースを得ることができます。ゼロのルール:管理する必要があるリソースはすべて自分のタイプにする必要があります。そのタイプはRule of Fiveに従わなければなりませんが、そのリソースのすべてのユーザーは5つの特別なメンバー関数のいずれかを書く必要はなく、それらのすべてをdefault
することができます。
3つのルールの例で紹介したPerson
クラスを使用して、 cstrings
リソース管理オブジェクトを作成できます。
class cstring {
private:
char* p;
public:
~cstring() { delete [] p; }
cstring(cstring const& );
cstring(cstring&& );
cstring& operator=(cstring const& );
cstring& operator=(cstring&& );
/* other members as appropriate */
};
一度これが分かれば、 Person
クラスはもっと簡単になります:
class Person {
cstring name;
int arg;
public:
~Person() = default;
Person(Person const& ) = default;
Person(Person&& ) = default;
Person& operator=(Person const& ) = default;
Person& operator=(Person&& ) = default;
/* other members as appropriate */
};
Person
の特別なメンバーは、明示的に宣言する必要はありません。コンパイラは、 Person
の内容に基づいて適切にデフォルトまたは削除します。したがって、以下はゼロのルールの例でもあります。
struct Person {
cstring name;
int arg;
};
cstring
がmove-only型で、 delete
dのコピーコンストラクタ/代入演算子を使用する場合、 Person
は自動的にmove-onlyになります。
ゼロのルールという用語は、 R。Martinho Fernandes
3つのルール
3つのルールでは、型にユーザ定義のコピーコンストラクタ、コピー代入演算子、またはデストラクタが必要な場合は、3つすべてを持つ必要があるということです。
ルールの理由は、3つのリソースのいずれかを必要とするクラスは、そのリソースを一貫して管理するためにいくつかのリソース(ファイルハンドル、動的に割り当てられたメモリなど)を管理する必要があるからです。コピー関数は、オブジェクトがオブジェクト間でどのようにコピーされるかを処理し、デストラクタはRAIIの原則に従ってリソースを破壊します。
文字列リソースを管理する型を考えてみましょう:
class Person
{
char* name;
int age;
public:
Person(char const* new_name, int new_age)
: name(new char[std::strlen(new_name) + 1])
, age(new_age)
{
std::strcpy(name, new_name);
}
~Person() {
delete [] name;
}
};
name
はコンストラクタに割り当てられていたため、デストラクタはメモリのリークを避けるためにデストラクタを解放します。しかし、そのようなオブジェクトがコピーされるとどうなりますか?
int main()
{
Person p1("foo", 11);
Person p2 = p1;
}
まず、 p1
が構築されます。次に、 p2
がp1
からコピーされます。しかし、C ++で生成されたコピーコンストラクタは、その型の各コンポーネントをそのままコピーします。これは、 p1.name
とp2.name
両方が同じ文字列を指していることを意味します。
main
終了すると、デストラクタが呼び出されます。最初のp2
のデストラクタが呼び出されます。文字列を削除します。次に、 p1
のデストラクタが呼び出されます。ただし、文字列はすでに削除されています。既に削除されたメモリ上のdelete
を呼び出すと、未定義の動作が発生します。
これを避けるには、適切なコピーコンストラクタを用意する必要があります。 1つのアプローチは、異なるPerson
インスタンスが同じ文字列データを共有する参照計数システムを実装することです。コピーが実行されるたびに、共有参照カウントがインクリメントされます。デストラクタは参照カウントをデクリメントし、カウントがゼロの場合にのみメモリを解放します。
あるいは、 価値セマンティクスとディープコピー動作を実装することもできます 。
Person(Person const& other)
: name(new char[std::strlen(other.name) + 1])
, age(other.age)
{
std::strcpy(name, other.name);
}
Person &operator=(Person const& other)
{
// Use copy and swap idiom to implement assignment
Person copy(other);
swap(copy); // assume swap() exchanges contents of *this and copy
return *this;
}
コピー代入演算子の実装は、既存のバッファを解放する必要があるため複雑です。コピーとスワップのテクニックは、新しいバッファを保持する一時オブジェクトを作成します。内容スワップ*this
およびcopy
して所有権を与えるcopy
元のバッファの。 copy
破壊は、関数が返すときに、以前に*this
によって所有されていたバッファを解放します。
自己割り当て保護
コピー代入演算子を書くときは、自己代入の際に働くことが非常に重要です。つまり、これを許可する必要があります:
SomeType t = ...;
t = t;
自己割り当ては、通常、そのような明白な方法では発生しません。典型的には、さまざまなコードシステムを通る迂回経路を介して行われます。割り当ての場所には、2つのPerson
ポインタまたは参照があり、それらが同じオブジェクトであることはわかりません。
あなたが書いたコピー代入演算子は、これを考慮に入れることができなければなりません。
これを行う一般的な方法は、割り当てロジックのすべてを次のような条件でラップすることです。
SomeType &operator=(const SomeType &other)
{
if(this != &other)
{
//Do assignment logic.
}
return *this;
}
注:自己割り当てについて考えることが重要であり、自己割り当てが発生したときにコードが正しく動作することを確認することが重要です。しかし、自己割り当ては非常にまれであり、実際に通常のケースを悲観的にするのを防ぐために最適化されています。通常のケースがはるかに一般的なので、自己割り当てのためのペシミングは、コード効率を大幅に低下させる可能性があります。
例として、代入演算子を実装するための通常の手法は、 copy and swap idiom
です。この技法の通常の実装では、自己割り当てがテストされることはありません(自己割り当てはコピーが作成されるため高価です)。その理由は、通常の場合のペシミーゼーションは(より頻繁に発生するので)はるかに高価であることが示されているからです。
移動代入演算子も自己割り当てから保護する必要があります。しかし、多くのそのような演算子のロジックはstd::swap
基づいており、これは同じメモリとのスワッピングをうまく処理できます。したがって、移動割り当てロジックが一連のスワップ操作に過ぎない場合は、自己割り当ての保護は必要ありません。
そうでない場合は、上記と同様の対策を講ずる必要があります。