1. ホーム
  2. c++

[解決済み] ルール・オブ・スリーとは?

2022-03-17 10:51:54

質問

  • とは何ですか? オブジェクトのコピー とはどういう意味ですか?
  • とは何ですか? コピーコンストラクタ コピー代入演算子 ?
  • 自分で宣言する必要があるのはどんな場合ですか?
  • 自分のオブジェクトがコピーされないようにするにはどうしたらいいですか?

解決方法は?

はじめに

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()
{
}

メンバー単位のコピーは、まさにこのケースで必要なことです。 nameage がコピーされるので、自己完結した独立した 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 メンバは単にポインタをコピーするだけです。 ではなく を指し示す文字配列です。 これにはいくつかの不愉快な影響があります。

  1. を経由して変更します。 a を経由して観測することができます。 b .
  2. 一度 b が破壊される。 a.name はダングリングポインタです。
  3. もし a が破壊された場合、ぶら下がったポインタを削除すると、次のようになります。 未定義の動作 .
  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 を含む配列が削除されます。 ソース の文字列を使用します。 と書くと x = x は、両方とも this->namethat.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つの法則が自分のコードに関係することはまずないでしょう。