3つのルールとは何ですか?
- オブジェクトのコピーとはどういう意味ですか?
- 何でコピーコンストラクタとコピー代入演算子は?
- いつ自分で宣言する必要がありますか?
- オブジェクトがコピーされないようにするにはどうすればよいですか?
回答
前書き
C ++は、ユーザー定義型の変数を値セマンティクスで処理します。これは、オブジェクトがさまざまなコンテキストで暗黙的にコピーされることを意味し、「オブジェクトのコピー」が実際に何を意味するかを理解する必要があります。
簡単な例を考えてみましょう。
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age) : name(name), age(age)
{
}
};
int main()
{
person a("Bjarne Stroustrup", 60);
person b(a); // What happens here?
b = a; // And here?
}
(name(name), age(age)
この部分に戸惑う場合、これはメンバー初期化子リストと呼ばれます。)
特別会員機能
person
オブジェクトをコピーするとはどういう意味ですか?このmain
関数は、2つの異なるコピーシナリオを示しています。初期化person b(a);
は、コピーコンストラクタによって実行されます。その仕事は、既存のオブジェクトの状態に基づいて新しいオブジェクトを構築することです。代入b = a
は、コピー代入演算子によって実行されます。ターゲットオブジェクトはすでに処理が必要な有効な状態にあるため、そのジョブは一般にもう少し複雑です。
コピーコンストラクタも代入演算子(またはデストラクタ)も自分で宣言していないので、これらは暗黙的に定義されています。標準からの引用:
[...]コピーコンストラクタとコピー割り当て演算子、[...]とデストラクタは特別なメンバー関数です。[注:プログラムが明示的に宣言していない場合、実装は一部のクラスタイプに対してこれらのメンバー関数を暗黙的に宣言します。それらが使用される場合、実装はそれらを暗黙的に定義します。[...]エンドノート] [n3126.pdfセクション12§1]
デフォルトでは、オブジェクトのコピーはそのメンバーのコピーを意味します。
非ユニオンクラスXの暗黙的に定義されたコピーコンストラクターは、そのサブオブジェクトのメンバーごとのコピーを実行します。[n3126.pdfセクション12.8§16]
非ユニオンクラスXに対して暗黙的に定義されたコピー代入演算子は、そのサブオブジェクトのメンバーごとのコピー代入を実行します。[n3126.pdfセクション12.8§30]
暗黙の定義
暗黙的に定義された特殊メンバー関数は次のperson
ようになります。
// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}
// 2. copy assignment operator
person& operator=(const person& that)
{
name = that.name;
age = that.age;
return *this;
}
// 3. destructor
~person()
{
}
メンバーごとのコピーは、まさにこの場合に必要なものです。name
そしてage
コピーされるので、自己完結型の独立したperson
オブジェクトを取得します。暗黙的に定義されたデストラクタは常に空です。この場合も、コンストラクターでリソースを取得しなかったため、これで問題ありません。メンバーのデストラクタは、person
デストラクタが終了した後に暗黙的に呼び出されます。
デストラクタの本体を実行し、本体内に割り当てられた自動オブジェクトを破棄した後、クラスXのデストラクタは、Xの直接[...]メンバーのデストラクタを呼び出します[n3126.pdf12.4§6]
リソースの管理
では、これらの特別なメンバー関数をいつ明示的に宣言する必要がありますか?クラスがリソースを管理するとき、つまり、クラスのオブジェクトがそのリソースを担当するとき。これは通常、リソースがコンストラクターで取得され(またはコンストラクターに渡され)、デストラクタで解放されることを意味します。
時間をさかのぼって、標準以前のC ++に戻りましょう。のようなものはありませんでしたstd::string
、そしてプログラマーはポインターに恋をしていました。person
クラスはこのように見えたかもしれません。
class person
{
char* name;
int age;
public:
// the constructor acquires a resource:
// in this case, dynamic memory obtained via new[]
person(const char* the_name, int the_age)
{
name = new char[strlen(the_name) + 1];
strcpy(name, the_name);
age = the_age;
}
// the destructor must release this resource via delete[]
~person()
{
delete[] name;
}
};
今日でも、人々はこのスタイルでクラスを作成し、問題を抱えています。「人をベクトルにプッシュすると、メモリエラーが発生します!」デフォルトでは、オブジェクトのコピーはそのメンバーのコピーを意味しますが、name
メンバーのコピーは単にポインタが指す文字配列ではなく、ポインタをコピーします!これにはいくつかの不快な影響があります。
- を介した変更は、を介して監視
a
できますb
。 - 一度
b
破壊されるa.name
と、ダングリングポインタです。 a
が破棄された場合、ダングリングポインタを削除すると、未定義の動作が発生します。- 割り当てでは、割り当ての
name
前に何を指していたかが考慮されていないため、遅かれ早かれ、至る所でメモリリークが発生します。
明示的な定義
メンバーごとのコピーには望ましい効果がないため、文字配列のディープコピーを作成するには、コピーコンストラクターとコピー割り当て演算子を明示的に定義する必要があります。
// 1. copy constructor
person(const person& that)
{
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
// 2. copy assignment operator
person& operator=(const person& that)
{
if (this != &that)
{
delete[] name;
// This is a dangerous point in the flow of execution!
// We have temporarily invalidated the class invariants,
// and the next statement might throw an exception,
// leaving the object in an invalid state :(
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
return *this;
}
初期化と割り当ての違いに注意してくださいname
。メモリリークを防ぐために、割り当てる前に古い状態を破棄する必要があります。また、フォームの自己割り当てから保護する必要がありx = x
ます。そのチェックがなければ、delete[] name
含む配列削除でしょうソースあなたが書くときので、文字列をx = x
、両方this->name
とthat.name
同じポインタを含んでいます。
例外安全性
残念ながら、new char[...]
メモリの枯渇が原因で例外をスローすると、このソリューションは失敗します。考えられる解決策の1つは、ローカル変数を導入してステートメントを並べ替えることです。
// 2. copy assignment operator
person& operator=(const person& that)
{
char* local_name = new char[strlen(that.name) + 1];
// If the above statement throws,
// the object is still in the same state as before.
// None of the following statements will throw an exception :)
strcpy(local_name, that.name);
delete[] name;
name = local_name;
age = that.age;
return *this;
}
これにより、明示的なチェックなしで自己割り当ても処理されます。この問題に対するさらに強力な解決策は、コピーアンドスワップのイディオムですが、ここでは例外安全性の詳細については説明しません。私は次の点を指摘するために例外についてのみ言及しました:リソースを管理するクラスを書くことは難しいです。
コピー不可能なリソース
ファイルハンドルやミューテックスなど、一部のリソースはコピーできないか、コピーしないでください。その場合、private
定義を与えずに、コピーコンストラクタとコピー代入演算子を宣言するだけです。
private:
person(const person& that);
person& operator=(const person& that);
または、boost::noncopyable
それらを継承するか、削除済みとして宣言することもできます(C ++ 11以降)。
person(const person& that) = delete;
person& operator=(const person& that) = delete;
3つのルール
リソースを管理するクラスを実装する必要がある場合があります。(1つのクラスで複数のリソースを管理しないでください。これは苦痛につながるだけです。)その場合は、次の3つのルールを覚えておいてください。
デストラクタ、コピーコンストラクタ、またはコピー割り当て演算子のいずれかを自分で明示的に宣言する必要がある場合は、おそらく3つすべてを明示的に宣言する必要があります。
(残念ながら、この「ルール」は、C ++標準または私が知っているコンパイラーによって強制されません。)
5つのルール
C ++ 11以降、オブジェクトには2つの特別なメンバー関数があります。ムーブコンストラクターとムーブ代入です。これらの機能も実装するための5つの状態のルール。
署名の例:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // 1/5: Copy Ctor
person(person &&) noexcept = default; // 4/5: Move Ctor
person& operator=(const person &) = default; // 2/5: Copy Assignment
person& operator=(person &&) noexcept = default; // 5/5: Move Assignment
~person() noexcept = default; // 3/5: Dtor
};
ゼロのルール
3/5のルールは、0/3/5のルールとも呼ばれます。ルールのゼロ部分は、クラスを作成するときに特別なメンバー関数を記述できないことを示しています。
助言
ほとんどの場合、リソースを自分で管理する必要はありませんstd::string
。これは、などの既存のクラスがすでに管理しているためです。std::string
メンバーを使用した単純なコードを、を使用した複雑でエラーが発生しやすい代替コードと比較するだけで、char*
納得できるはずです。生のポインターメンバーから離れている限り、3つのルールが自分のコードに関係する可能性はほとんどありません。
三つのルールは基本的に言って、C ++のための経験則であるが、
あなたのクラスがいずれかを必要とする場合
- コピーコンストラクタ、
- 代入演算子、
- またはデストラクタ、
明示的に定義されている場合、3つすべてが必要になる可能性があります。
これは、通常、3つすべてがリソースの管理に使用され、クラスがリソースを管理する場合は、通常、コピーと解放を管理する必要があるためです。
クラスが管理するリソースをコピーするための適切なセマンティクスがない場合は、コピーコンストラクターと代入演算子をとして宣言(定義しない)して、コピーを禁止することを検討してくださいprivate
。
(C ++標準の次の新しいバージョン(C ++ 11)は、C ++に移動セマンティクスを追加することに注意してください。これにより、3つのルールが変更される可能性があります。 3つのルールについて。)
ビッグスリーの法則は上記のとおりです。
平易な英語で、それが解決する種類の問題の簡単な例:
デフォルト以外のデストラクタ
コンストラクタにメモリを割り当てたので、それを削除するにはデストラクタを作成する必要があります。そうしないと、メモリリークが発生します。
これは仕事だと思うかもしれません。
問題は、オブジェクトのコピーが作成された場合、そのコピーが元のオブジェクトと同じメモリを指すことです。
これらの1つがデストラクタのメモリを削除すると、もう1つは無効なメモリへのポインタ(これはダングリングポインタと呼ばれます)を使用しようとすると、問題が発生します。
したがって、コピーコンストラクターを作成して、新しいオブジェクトに独自のメモリを割り当てて破棄します。
代入演算子とコピーコンストラクタ
コンストラクターのメモリーをクラスのメンバーポインターに割り当てました。このクラスのオブジェクトをコピーすると、デフォルトの代入演算子とコピーコンストラクターがこのメンバーポインターの値を新しいオブジェクトにコピーします。
これは、新しいオブジェクトと古いオブジェクトが同じメモリを指しているため、一方のオブジェクトで変更すると、もう一方のオブジェクトでも変更されることを意味します。一方のオブジェクトがこのメモリを削除すると、もう一方のオブジェクトはそれを使用しようとし続けます-eek。
これを解決するには、独自のバージョンのコピーコンストラクターと代入演算子を作成します。ご使用のバージョンでは、新しいオブジェクトに個別のメモリを割り当て、アドレスではなく最初のポインタが指している値をコピーします。
基本的に、(デフォルトのデストラクタではなく)デストラクタがある場合は、定義したクラスにメモリが割り当てられていることを意味します。クラスが外部のクライアントコードまたはユーザーによって使用されているとします。
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
MyClassにプリミティブ型のメンバーがいくつかある場合、デフォルトの代入演算子は機能しますが、ポインターメンバーと、代入演算子を持たないオブジェクトがある場合、結果は予測できません。したがって、クラスのデストラクタで削除するものがある場合は、ディープコピー演算子が必要になる可能性があります。つまり、コピーコンストラクタと代入演算子を提供する必要があります。
オブジェクトのコピーとはどういう意味ですか?オブジェクトをコピーする方法はいくつかあります。最も参照している可能性が高い2種類について説明します。ディープコピーとシャローコピーです。
私たちはオブジェクト指向言語を使用しているので(または少なくともそう想定している)、メモリの一部が割り当てられているとしましょう。オブジェクト指向言語であるため、割り当てたメモリのチャンクは通常、独自の型とプリミティブで作成されたプリミティブ変数(int、char、bytes)または定義したクラスであるため、簡単に参照できます。したがって、次のような車のクラスがあるとしましょう。
class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;
public changePaint(String newColor)
{
this.sPrintColor = newColor;
}
public Car(String model, String make, String color) //Constructor
{
this.sPrintColor = color;
this.sModel = model;
this.sMake = make;
}
public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}
public Car(const Car &other) // Copy Constructor
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
if(this != &other)
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
return *this;
}
}
ディープコピーとは、オブジェクトを宣言してから、オブジェクトの完全に別個のコピーを作成する場合です... 2つの完全なメモリセットに2つのオブジェクトが含まれることになります。
Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.
それでは、奇妙なことをしましょう。car2が間違ってプログラムされているか、car1が構成されている実際のメモリを共有することを意図的に意図しているとしましょう。(これを行うのは通常間違いであり、クラスでは通常、それが説明されている包括的なものです。)car2について尋ねるときはいつでも、car1のメモリ空間へのポインタを本当に解決しているふりをしてください...それは多かれ少なかれ浅いコピーですです。
//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.
Car car1 = new Car("ford", "mustang", "red");
Car car2 = car1;
car2.changePaint("green");//car1 is also now green
delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve
the address of where car2 exists and delete the memory...which is also
the memory associated with your car.*/
car1.changePaint("red");/*program will likely crash because this area is
no longer allocated to the program.*/
したがって、どの言語で書いているかに関係なく、オブジェクトのコピーに関しては、ほとんどの場合、ディープコピーが必要になるため、意味に十分注意してください。
コピーコンストラクターとコピー割り当て演算子とは何ですか?私はすでに上記でそれらを使用しました。Car car2 = car1;
基本的に、変数を宣言して1行で割り当てる場合など、コードを入力するとコピーコンストラクターが呼び出されます。これは、コピーコンストラクターが呼び出されるときです。代入演算子は、等号を使用したときに発生するものです- car2 = car1;
。通知car2
は同じステートメントで宣言されていません。これらの操作のために作成するコードの2つのチャンクは、おそらく非常に似ています。実際、典型的なデザインパターンには、最初のコピー/割り当てが正当であることに満足したら、すべてを設定するために呼び出す別の関数があります。私が書いた長いコードを見ると、関数はほぼ同じです。
いつ自分で宣言する必要がありますか?共有または本番用のコードを何らかの方法で記述していない場合は、実際には必要なときにのみ宣言する必要があります。「誤って」使用することを選択し、それを作成しなかった場合、つまりコンパイラのデフォルトを取得した場合は、プログラム言語が何をするかを知っておく必要があります。たとえば、コピーコンストラクターを使用することはめったにありませんが、代入演算子のオーバーライドは非常に一般的です。足し算や引き算などの意味も上書きできることをご存知ですか?
オブジェクトがコピーされないようにするにはどうすればよいですか?プライベート関数を使用してオブジェクトにメモリを割り当てることが許可されているすべての方法をオーバーライドすることは、妥当な出発点です。本当に他の人にコピーさせたくない場合は、それを公開して、例外をスローし、オブジェクトをコピーしないことでプログラマーに警告することができます。
いつ自分で宣言する必要がありますか?
3つのルールでは、次のいずれかを宣言すると、
- コピーコンストラクタ
- コピー割り当て演算子
- デストラクタ
次に、3つすべてを宣言する必要があります。コピー操作の意味を引き継ぐ必要性は、ほとんどの場合、ある種のリソース管理を実行するクラスに起因するという観察から生まれました。これは、ほとんどの場合、それを意味します。
一方のコピー操作で実行されていたリソース管理は、おそらくもう一方のコピー操作で実行する必要があり、
クラスデストラクタは、リソースの管理にも参加します(通常はリソースを解放します)。管理される古典的なリソースはメモリでした。これが、メモリを管理するすべての標準ライブラリクラス(動的メモリ管理を実行するSTLコンテナなど)がすべて「ビッグ3」(コピー操作とデストラクタの両方)を宣言する理由です。
Rule of Threeの結果は、ユーザーが宣言したデストラクタの存在は、単純なメンバーごとのコピーがクラス内のコピー操作に適切である可能性が低いことを示しています。つまり、クラスがデストラクタを宣言している場合、コピー操作は正しいことを行わないため、おそらく自動的に生成されるべきではないことを示唆しています。C ++ 98が採用されたとき、この推論の行の重要性は十分に理解されていなかったため、C ++ 98では、ユーザー宣言デストラクタの存在は、コピー操作を生成するコンパイラの意欲に影響を与えませんでした。これはC ++ 11でも引き続き当てはまりますが、コピー操作が生成される条件を制限すると、レガシーコードが破損しすぎるためです。
オブジェクトがコピーされないようにするにはどうすればよいですか?
コピーコンストラクターとコピー代入演算子をプライベートアクセス指定子として宣言します。
class MemoryBlock
{
public:
//code here
private:
MemoryBlock(const MemoryBlock& other)
{
cout<<"copy constructor"<<endl;
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
return *this;
}
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
C ++ 11以降では、コピーコンストラクタと代入演算子の削除を宣言することもできます
class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
既存の回答の多くは、すでにコピーコンストラクタ、代入演算子、およびデストラクタに触れています。ただし、ポストC ++ 11では、移動セマンティクスの導入により、これが3を超えて拡張される可能性があります。
最近、Michael Claisseが、このトピックに触れる講演を行いました。 http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
C ++の3つのルールは、次のメンバー関数の1つに明確な定義がある場合、プログラマーは他の2つのメンバー関数を一緒に定義する必要があるという3つの要件の設計と開発の基本原則です。つまり、デストラクタ、コピーコンストラクタ、コピー割り当て演算子の3つのメンバー関数が不可欠です。
C ++のコピーコンストラクターは特別なコンストラクターです。これは、既存のオブジェクトのコピーに相当する新しいオブジェクトである新しいオブジェクトを構築するために使用されます。
コピー代入演算子は、通常、同じタイプのオブジェクトの他のオブジェクトに既存のオブジェクトを指定するために使用される特別な代入演算子です。
簡単な例があります:
// default constructor
My_Class a;
// copy constructor
My_Class b(a);
// copy constructor
My_Class c = a;
// copy assignment operator
b = a;