web-dev-qa-db-ja.com

3の法則は何ですか?

  • オブジェクトをコピーする はどういう意味ですか?
  • コピーコンストラクタ および コピー代入演算子 とは何ですか。
  • 自分で宣言する必要があるのはいつですか。
  • オブジェクトがコピーされないようにするにはどうすればよいですか。
1989
fredoverflow

前書き

C++は、ユーザー定義型の変数をvalueセマンティクスで処理します。これは、オブジェクトがさまざまなコンテキストで暗黙的にコピーされることを意味し、「オブジェクトのコピー」が実際に意味することを理解する必要があります。

簡単な例を考えてみましょう。

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);copyコンストラクターによって実行されます。その仕事は、既存のオブジェクトの状態に基づいて新しいオブジェクトを構築することです。割り当てb = aは、copy割り当て演算子によって実行されます。通常、ターゲットオブジェクトは処理が必要な有効な状態にあるため、その作業は一般に少し複雑です。

コピーコンストラクタも代入演算子(デストラクタ)も自分で宣言していないため、これらは暗黙的に定義されています。標準からの引用:

[...]コピーコンストラクタおよびコピー代入演算子、[...]およびデストラクタは特別なメンバー関数です。 [プログラムが明示的に宣言しない場合、実装はいくつかのクラス型に対してこれらのメンバー関数を暗黙的に宣言します。それらが使用される場合、実装はそれらを暗黙的に定義します。 [...]end note] [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()
{
}

この場合、メンバー単位のコピーがまさに必要です。nameageがコピーされるため、自己完結型の独立したpersonオブジェクトが取得されます。暗黙的に定義されたデストラクタは常に空です。コンストラクターでリソースを取得しなかったため、この場合も問題ありません。メンバーのデストラクタは、personデストラクタが終了した後に暗黙的に呼び出されます。

デストラクタの本体を実行し、本体内に割り当てられた自動オブジェクトを破棄した後、クラスXのデストラクタは、Xの直接[...]メンバーのデストラクタを呼び出します[n3126.pdf 12.4§6]

リソースを管理する

それでは、いつこれらの特別なメンバー関数を明示的に宣言すべきでしょうか?クラスがリソースを管理する場合、つまり、クラスのオブジェクトがそのリソースに対してresponsibleである場合。通常、リソースはコンストラクターでacquired(またはコンストラクターに渡されます)、デストラクタでreleasedになります。

以前の標準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メンバーをコピーすることは、ポインターをコピーするだけで、notそれが指す文字配列ではありません!これにはいくつかの不快な影響があります。

  1. a経由の変更は、b経由で確認できます。
  2. bが破棄されると、a.nameはダングリングポインターになります。
  3. aが破棄された場合、ダングリングポインターを削除すると ndefined behavior が生成されます。
  4. 割り当てでは、割り当て前に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は、source文字列を含む配列を削除します。これは、x = xを記述すると、this->namethat.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;
}

これはまた、明示的なチェックなしで自己割り当てを処理します。この問題に対するさらに堅牢なソリューションは copy-and-swap idiom ですが、ここでは例外の安全性の詳細には触れません。次の点を説明するために例外についてのみ言及しました:リソースを管理するクラスの作成は難しいです。

コピー不可のリソース

ファイルハンドルやミューテックスなど、一部のリソースはコピーできないか、コピーすべきではありません。その場合、定義を与えずに、コピーコンストラクタとコピー代入演算子を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つのルール

リソースを管理するクラスを実装する必要がある場合があります。 (単一のクラスで複数のリソースを管理しないでください。これは苦痛につながるだけです。)その場合、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;                // Copy Ctor
    person(person &&) noexcept = default;            // Move Ctor
    person& operator=(const person &) = default;     // Copy Assignment
    person& operator=(person &&) noexcept = default; // Move Assignment
    ~person() noexcept = default;                    // Dtor
};

ゼロのルール

3/5のルールは、0/3/5のルールとも呼ばれます。ルールのゼロ部分は、クラスを作成するときに特別なメンバー関数を記述しないことを許可していることを示しています。

助言

ほとんどの場合、std::stringなどの既存のクラスがすでにあなたのためにリソースを管理しているため、リソースを自分で管理する必要はありません。 std::stringメンバーを使用する単純なコードと、char*を使用する複雑でエラーが発生しやすい代替コードを比較するだけで、納得できるはずです。生のポインタメンバから離れている限り、3つのルールは独自のコードに関係する可能性は低いです。

1683
fredoverflow

Three of 3 はC++の経験則で、基本的には次のとおりです。

クラスに以下のいずれかが必要な場合

  • コピーコンストラクタ
  • 代入演算子
  • または デストラクタ

厳密に定義すると、 それらの3つすべて が必要になるでしょう。

その理由は、それらの3つすべてが通常リソースの管理に使用され、クラスがリソースを管理する場合、通常はコピーと解放を管理する必要があるためです。

クラスが管理しているリソースをコピーするのに良い意味がない場合は、コピーコンストラクタと代入演算子をprivateとして宣言することでコピーを禁止することを検討してください(define)。

(C++ 11の次期バージョンであるC++ 11はC++にmoveセマンティクスを追加しているため、Rule of Threeが変更される可能性があることに注意してください。 3の法則について)

473
sbi

ビッグスリーの法則は上記で指定された通りです。

平易な英語で、それが解決する問題の種類の簡単な例:

デフォルト以外のデストラクタ

あなたはあなたのコンストラクタにメモリを割り当てたので、それを削除するためにデストラクタを書く必要があります。それ以外の場合は、メモリリークが発生します。

あなたはこれが仕事で終わったと思うかもしれません。

問題は、あなたのオブジェクトからコピーが作成された場合、そのコピーは元のオブジェクトと同じメモリを指すことになるでしょう。

いったん、これらのうちの1つがデストラクタ内のメモリを削除すると、それを使用しようとすると、無効なメモリへのポインタ(これはダングリングポインタと呼ばれます)を持ちます。

したがって、新しいオブジェクトに独自のメモリを割り当てて破壊するようにコピーコンストラクタを作成します。

代入演算子とコピーコンストラクタ

コンストラクタ内のメモリをクラスのメンバポインタに割り当てました。このクラスのオブジェクトをコピーすると、デフォルトの代入演算子とコピーコンストラクタがこのメンバポインタの値を新しいオブジェクトにコピーします。

つまり、新しいオブジェクトと古いオブジェクトは同じメモリを指すので、一方のオブジェクトで変更すると、もう一方のオブジェクトに対しても変更されます。 1つのオブジェクトがこのメモリを削除すると、もう一方のオブジェクトがそれを使用しようとし続けます。

これを解決するには、独自のバージョンのコピーコンストラクタと代入演算子を書きます。あなたのバージョンは新しいオブジェクトに別々のメモリを割り当て、そのアドレスではなく最初のポインタが指している値全体にコピーします。

149
Stefan

基本的に、デストラクタ(デフォルトのデストラクタではない)がある場合、それはあなたが定義したクラスにメモリ割り当てがあることを意味します。クラスが何らかのクライアントコードまたはあなたによって外部で使用されると仮定します。

    MyClass x(a, b);
    MyClass y(c, d);
    x = y; // This is a shallow copy if assignment operator is not provided

MyClassにプリミティブ型のメンバーがいくつかあるだけの場合、デフォルトの代入演算子は機能しますが、代入オペレーターを持たないポインターメンバーとオブジェクトがある場合は、結果は予測できません。したがって、クラスのデストラクタに削除するものがある場合、ディープコピー演算子が必要になる可能性があります。つまり、コピーコンストラクタと代入演算子を提供する必要があります。

41
fatma.ekici

オブジェクトをコピーするとはどういう意味ですか?オブジェクトをコピーするにはいくつかの方法があります - あなたが最もよく参照している2種類について話しましょう - ディープコピーとシャローコピー。

私たちはオブジェクト指向言語なので(少なくともそう仮定しているのであれば)、メモリが割り当てられているとしましょう。これはオブジェクト指向言語なので、通常はプリミティブ変数(int、chars、bytes)または定義したクラスであるため、割り当てたメモリのチャンクを簡単に参照できます。次のようにCarのクラスがあるとしましょう。

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つのコードは非常によく似ています。実際、典型的なデザインパターンには、最初のコピー/割り当てが正当であることを確認したら、すべてを設定するために呼び出す別の関数があります。

自分で宣言する必要があるのはいつですか。何らかの方法で共有またはプロダクションのためのコードを書いていないのであれば、本当に必要なときに宣言するだけです。プログラムの言語を「偶然に」使用することを選択し、使用しない場合は、プログラムの言語が何をするのかを知っておく必要があります。あなたはコンパイラのデフォルトを取得します。私はめったにコピーコンストラクタを使用しませんが、代入演算子のオーバーライドは非常に一般的です。足し算、引き算なども同様に上書きできることをご存知ですか。

オブジェクトがコピーされないようにするにはどうすればよいですか。プライベート関数を使用してオブジェクトにメモリを割り当てるために許可されているすべての方法をオーバーライドすることは、合理的な出発点です。他人にそれらをコピーさせたくない場合は、それを公開して、例外をスローしてオブジェクトをコピーしないようにしてプログラマに警告することもできます。

34
user1701047

自分で宣言する必要があるのはいつですか。

3のルールはあなたが宣言した場合

  1. コピーコンストラクタ
  2. コピー代入演算子
  3. デストラクタ

それからあなたは3つすべてを宣言するべきです。コピー操作の意味を引き継ぐ必要性は、ほとんどの場合、ある種のリソース管理を実行するクラスから生じているという観察から生まれました。

  • あるコピー操作で行われていたリソース管理は、他のコピー操作でも行われる必要がありました。

  • クラスデストラクタもリソースの管理に参加します(通常はリソースを解放します)。管理される古典的なリソースはメモリでした、そしてそれがメモリを管理するすべての標準ライブラリクラス(例えば動的メモリ管理を実行するSTLコンテナ)がすべて「ビッグ3」を宣言する理由です:コピー操作とデストラクタの両方。

3の規則の結果 - ユーザー宣言のデストラクタが存在することは、単純なメンバーワイズコピーがクラス内のコピー操作に適しているとは考えにくいことを示しています。つまり、クラスがデストラクタを宣言した場合、コピー操作はおそらく自動的には生成されないはずであることを示唆しています。なぜなら、それらは正しいことをしないからです。 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);
}
23
Ajay yadav

既存の回答の多くは、すでにコピーコンストラクタ、代入演算子、デストラクタに触れています。ただし、ポストC++ 11では、移動セマンティックの導入により、これが3を超えて拡張される可能性があります。

最近Michael Claisseがこのトピックに触れた講演を行いました: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class

14
wei

C++の3の規則は、次のいずれかのメンバ関数に明確な定義がある場合、プログラマが他の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;
9
Marcus Thornton