[解決済み] ルール・オブ・スリーとは?
質問
- とは何ですか? オブジェクトのコピー とはどういう意味ですか?
- とは何ですか? コピーコンストラクタ と コピー代入演算子 ?
- 自分で宣言する必要があるのはどんな場合ですか?
- 自分のオブジェクトがコピーされないようにするにはどうしたらいいですか?
解決方法は?
はじめに
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.pdf 12.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[...]
は、メモリ枯渇による例外を投げます。
ローカル変数を導入し、ステートメントを並べ替えるのも一つの解決策です。
// 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
またはdeletedとして宣言します(C++11以上)。
person(const person& that) = delete;
person& operator=(const person& that) = delete;
3つのルール
リソースを管理するクラスを実装する必要がある場合があります。 (1つのクラスで複数のリソースを管理してはいけません。 これは苦痛をもたらすだけです)。 そのような場合は 三の法則 :
デストラクタを明示的に宣言する必要がある場合。 コピーコンストラクタ、コピー代入演算子 の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の法則とも呼ばれる。ルールの0部分は、クラスを作成する際に特別なメンバー関数を一切記述しないことが許されていることを述べています。
アドバイス
ほとんどの場合、リソースを自分で管理する必要はありません。
のような既存のクラスがあるためです。
std::string
は、すでにあなたのためにそれを行います。
この例では
std::string
メンバー
を使用した複雑でエラーを起こしやすい方法とは異なります。
char*
と言えば、納得してもらえるはずです。
生のポインタ・メンバに手を出さない限り、3つの法則が自分のコードに関係することはまずないでしょう。
関連
-
[解決済み】Visual Studio 2013および2015でC++コンパイラーエラーC2280「削除された関数を参照しようとした」が発生する
-
[解決済み】システムが指定されたファイルを見つけられませんでした。
-
[解決済み] gdbを使用してもデバッグシンボルが見つからない
-
[解決済み] explicit キーワードの意味は?
-
[解決済み] コピーアンドスワップ慣用句とは?
-
[解決済み] C++11では、標準化されたメモリモデルが導入されました。その意味するところは?そして、C++プログラミングにどのような影響を与えるのでしょうか?
-
[解決済み] スマートポインターとは何ですか?
-
[解決済み] ムーブセマンティクスとは何ですか?
-
[解決済み] 仮想デストラクタはいつ使うのか?
-
[解決済み】C/C++の"-->"演算子とは何ですか?
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み】コンストラクターでのエラー:識別子を期待されますか?
-
[解決済み】致命的なエラー LNK1169: ゲームプログラミングで1つ以上の多重定義されたシンボルが発見された
-
[解決済み】関数名の前に期待されるイニシャライザー
-
[解決済み】「corrupted size vs. prev_size」glibc エラーを理解する。
-
[解決済み】浮動小数点例外エラーが発生する: 8
-
[解決済み】Visual Studio 2013および2015でC++コンパイラーエラーC2280「削除された関数を参照しようとした」が発生する
-
[解決済み】#include<iostream>は存在するのですが、「識別子 "cout "は未定義です」というエラーが出ます。なぜですか?
-
[解決済み】ファイルから整数を読み込んで配列に格納する C++ 【クローズド
-
[解決済み] 非静的データメンバの無効な使用
-
[解決済み] to_string は std のメンバーではない、と g++ が言っている (mingw)