コピーアンドスワップの慣用句とは

c++ copy-constructor assignment-operator c++-faq copy-and-swap


この慣用句は何で、どんな時に使うべきなのか?どのような問題を解決するのですか?C++11を使うとイディオムは変わりますか?

いろいろなところで言及されていますが、単数形の「それは何か」という質問と回答はありませんでしたので、こちらになります。以前に言及されていた場所の一部をご紹介します。




Answer 1 GManNickG


Overview

なぜコピーアンドスワップの熟語が必要なのか?

リソースを管理するクラス(スマートポインターのようなラッパー)は、The Big Threeを実装する必要があります。コピーコンストラクターとデストラクターの目標と実装は単純ですが、コピー代入演算子は間違いなく最も微妙で難しいものです。どのようにすればよいですか?どのような落とし穴を避ける必要がありますか?

コピーおよびスワップイディオムはソリューションであり、そしてエレガントに二つのことを達成するために、代入演算子を支援:回避コードの重複を、そして提供する強力な例外保証を

どうやって使うのか?

概念的には、コピーコンストラクターの機能を使用してデータのローカルコピーを作成し、次にコピーされたデータを swap 関数で取得して、古いデータを新しいデータと交換します。その後、一時コピーは破棄され、古いデータが一緒に破棄されます。新しいデータのコピーが残ります。

コピーアンドスワップイディオムを使用するには、3つのものが必要です。有効なコピーコンストラクター、有効なデストラクター(どちらもラッパーの基礎であり、いずれにしても完全である必要があります)、および swap 関数です。

スワップ関数は、クラスの2つのオブジェクト(メンバーごと)をスワップする非スロー関数です。独自のものを提供する代わりに std::swap を使用したくなるかもしれませんが、これは不可能です。 std::swap は、その実装内でcopy-constructorおよびcopy-assignment演算子を使用しており、最終的にそれ自体で代入演算子を定義しようとしています!

(それだけでなく、 swap への修飾されていない呼び出しは、カスタムのswap演算子を使用し、 std::swap が必要とするクラスの不要な構築と破棄をスキップします。)


徹底した解説

The goal

具体的なケースを考えてみましょう。何の役にも立たないクラスで、動的な配列を管理したいとします。まず、作業用のコンストラクタ、コピーコンストラクタ、デストラクタから始めます。

#include <algorithm> // std::copy
#include <cstddef> // std::size_t

class dumb_array
{
public:
    // (default) constructor
    dumb_array(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new int[mSize]() : nullptr)
    {
    }

    // copy-constructor
    dumb_array(const dumb_array& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr),
    {
        // note that this is non-throwing, because of the data
        // types being used; more attention to detail with regards
        // to exceptions must be given in a more general case, however
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }

    // destructor
    ~dumb_array()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};

このクラスは、配列をほぼ正常に管理しますが、 operator= が正しく機能する必要があります。

失敗した解決策

ナイーブな実装がどのように見えるかというと、こんな感じです。

// the hard part
dumb_array& operator=(const dumb_array& other)
{
    if (this != &other) // (1)
    {
        // get rid of the old data...
        delete [] mArray; // (2)
        mArray = nullptr; // (2) *(see footnote for rationale)

        // ...and put in the new
        mSize = other.mSize; // (3)
        mArray = mSize ? new int[mSize] : nullptr; // (3)
        std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
    }

    return *this;
}

そして、私たちは終わったと言います。これはリークなしでアレイを管理するようになりました。ただし、コード内で (n) と順番にマークされている3つの問題があります。

  1. 1つ目は自己割当テストです。このチェックには二つの目的があります:自己割り当てで不要なコードを実行しないようにするための簡単な方法です。しかし、それ以外のすべての場合において、これは単にプログラムを遅くし、コード内のノイズとして機能するだけです。演算子がこのチェックなしで正常に動作するようになれば、より良いでしょう。

  2. 2つ目は、基本的な例外保証のみを提供することです。場合は new int[mSize] 失敗し、 *this 変更されています。(つまり、サイズが間違っていて、データがなくなっています!)強力な例外保証のためには、次のようなものである必要があります。

    dumb_array& operator=(const dumb_array& other)
    {
        if (this != &other) // (1)
        {
            // get the new data ready before we replace the old
            std::size_t newSize = other.mSize;
            int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
            std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
    
            // replace the old data (all are non-throwing)
            delete [] mArray;
            mSize = newSize;
            mArray = newArray;
        }
    
        return *this;
    }
  3. コードが拡張された!これが3つ目の問題、コードの重複です。私たちの代入演算子は、私たちがすでに他の場所で書いたすべてのコードを効果的に複製してしまいます。

私たちの場合、その核心部分は2行(割り当てとコピー)だけですが、より複雑なリソースでは、このコードの肥大化はかなり面倒なものになります。決して繰り返さないように努力すべきです。

(1つのリソースを正しく管理するためにこれだけのコードが必要な場合、クラスが複数のリソースを管理する場合はどうなりますか?これは有効な懸念事項であるように見えるかもしれませんが、実際には重要な try / catch 句が必要ですが、これは問題ではありません。これは、クラスは1つのリソースのみを管理する必要があるためです!)

成功したソリューション

前述のように、コピーアンドスワップイディオムはこれらの問題をすべて修正します。しかし、現時点では、1つを除くすべての要件があります。それは、 swap 関数です。3つのルールは、コピーコンストラクター、代入演算子、およびデストラクターの存在を伴うことに成功していますが、実際には「ビッグスリーアンドハーフ」と呼ばれるべきです。クラスがリソースを管理するときはいつでも、 swap を提供することにも意味があります。関数。

クラスにスワップ機能を追加する必要があり、次のように実行します†:

class dumb_array
{
public:
    // ...

    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        // enable ADL (not necessary in our case, but good practice)
        using std::swap;

        // by swapping the members of two objects,
        // the two objects are effectively swapped
        swap(first.mSize, second.mSize);
        swap(first.mArray, second.mArray);
    }

    // ...
};

ここでは説明の理由は public friend swap 。)今、我々は交換することができないだけで dumb_array さんが、しかし一般的にはスワップは、より効率的にすることができます。配列全体を割り当ててコピーするのではなく、ポインタとサイズを交換するだけです。機能と効率のこのボーナスを除けば、コピーアンドスワップイディオムを実装する準備が整いました。

さらっと説明すると、私たちの代入演算子は

dumb_array& operator=(dumb_array other) // (1)
{
    swap(*this, other); // (2)

    return *this;
}

そして、これだ! 3つの問題に一気にエレガントに取り組むことができます。

なぜ効果があるのか?

最初に重要な選択に気づきます。パラメーター引数はby-valueを取ります。同じように簡単に次のことを実行できます(実際、イディオムの単純な実装の多くが実行します)。

dumb_array& operator=(const dumb_array& other)
{
    dumb_array temp(other);
    swap(*this, temp);

    return *this;
}

重要な最適化の機会を失う。それだけでなく、この選択は、後で説明するC ++ 11では重要です。(一般的な注意として、非常に便利なガイドラインは次のとおりです:関数内で何かのコピーを作成する場合は、コンパイラーにパラメーターリストで実行させます。‡)

どちらにしても、この方法でリソースを取得することが、コードの重複をなくす鍵となります:コピーを作成するためにコピーコンストラクタのコードを使うことができます。これでコピーができたので、スワップの準備ができました。

関数に入ると、すべての新しいデータがすでに割り当てられ、コピーされ、使用できる状態になっていることを確認します。これが、私たちに強力な例外保証を無料で提供するものです。コピーの構築が失敗した場合、関数に入ることさえないため、 *this の状態を変更することはできません。(以前は強力な例外保証のために手動で行っていましたが、コンパイラーが今やってくれています。

swap はスローされないため、この時点ではホームフリーです。現在のデータをコピーしたデータと交換して、状態を安全に変更します。古いデータは一時ファイルに入れられます。関数が戻ると、古いデータが解放されます。(パラメーターのスコープが終了し、そのデストラクターが呼び出される場所。)

イディオムはコードを繰り返さないため、オペレーター内にバグを導入することはできません。これは、自己代入チェックの必要がなくなるため、 operator= 単一の統一実装が可能になることを意味しています。(さらに、非自己割り当てのパフォーマンスが低下することはもうありません。)

そして、それがコピーアンドスワップという慣用句です。

C++11はどうですか?

C ++の次のバージョンであるC ++ 11では、リソースの管理方法に1つの非常に重要な変更が加えられています。3つのルールは4つのルール(および半分)になりました。どうして?リソースをコピーして構築できる必要があるだけでなく、リソースも移動して構築する必要があります

幸いなことに、これは簡単です。

class dumb_array
{
public:
    // ...

    // move constructor
    dumb_array(dumb_array&& other) noexcept ††
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

ここで何が起こっているのでしょうか?移動-構築の目的を思い出してください:クラスの別のインスタンスからリソースを取り、それを割り当て可能で破壊可能であることが保証された状態に残すことです。

だから私たちがやったことは簡単です:デフォルトのコンストラクター(C ++ 11機能)を介して初期化し、 other と交換します。クラスのデフォルトで構築されたインスタンスを安全に割り当て、破棄できることがわかっているので、スワッピング後に other インスタンスでも同じことができるようになります。

(一部のコンパイラーはコンストラクターの委任をサポートしていないことに注意してください。この場合、手動でデフォルトでクラスを作成する必要があります。これは残念ながら幸運なことですが簡単な作業です。)

なぜそれが効くのか?

クラスに変更を加える必要があるのはこれだけです。パラメータを参照ではなく値にするという重要な決定を忘れないでください。

dumb_array& operator=(dumb_array other); // (1)

これで、 other が右辺値で初期化されている場合は、move-constructedになります。完璧です。C ++ 03が引数by-valueを使用してコピーコンストラクター機能を再利用できるように、C ++ 11 も適切な場合にmove-constructor を自動的に選択します。(そしてもちろん、以前にリンクされた記事で述べられているように、値のコピー/移動は単に完全に省略されるかもしれません。)

そして、コピー&スワップという慣用句は、このように結論づけられています。


Footnotes

* mArray をnullに設定するのはなぜですか?演算子のコードがさらにスローされると、 dumb_array のデストラクタが呼び出される可能性があるためです。nullに設定せずにそれが発生した場合、すでに削除されているメモリを削除しようとします!nullを削除することは操作ではないので、nullに設定することでこれを回避します。

† タイプに std::swap を特化し、自由関数 swap と並んでクラス内 swap 提供するなどの他の主張があります。しかし、これはすべて不要です。 swap 適切な使用は、無資格による呼び出すと、関数がADLを通じて見つかります。1つの関数が行います。

‡理由は単純です。リソースを自分に割り当てたら、それを必要な場所に入れ替えたり移動したりできます(C ++ 11)。また、パラメーターリストにコピーを作成することで、最適化を最大化できます。

††ムーブコンストラクターは通常 noexcept でなければなりません。そうでない場合、一部のコード(例: std::vector サイズ変更ロジック)は、ムーブが意味をなす場合でもコピーコンストラクターを使用します。もちろん、内部のコードが例外をスローしない場合を除いて、それだけをマークしないでください。




Answer 2 sbi


割り当ては、基本的には2つのステップです。オブジェクトの古い状態破棄し、他のオブジェクトの状態のコピーとして新しい状態を構築します。

基本的に、それはデストラクタコピーコンストラクタが行うことなので、最初のアイデアは作業をそれらに委任することです。ただし、破壊は失敗してはならないため、構築は可能ですが、実際には逆にしたいと思います最初に構築部分実行し、成功した場合は破壊部分を実行します。コピーアンドスワップイディオムは、まさにそれを行う方法です。まず、クラスのコピーコンストラクターを呼び出して一時オブジェクトを作成し、次にそのデータを一時オブジェクトと交換し、次に一時クラスのデストラクタに古い状態を破棄させます。
以来 swap() 失敗することはありませんすることになって、失敗するかもしれない唯一の部分は、コピーの建設です。それが最初に実行され、失敗した場合、ターゲットオブジェクトでは何も変更されません。

洗練された形では、コピーアンドスワップは、代入演算子の(非参照)パラメータを初期化することでコピーを実行させることで実装されます。

T& operator=(T tmp)
{
    this->swap(tmp);
    return *this;
}



Answer 3 Tony Delroy


すでにいくつかの良い答えがあります。私はにそれらが欠けていると思うものに焦点を当てます-コピーアンドスワップイディオムの「短所」の説明...

コピーアンドスワップの慣用句とは?

スワップ関数の観点から代入演算子を実装する方法。

X& operator=(X rhs)
{
    swap(rhs);
    return *this;
}

というのが基本的な考え方です。

  • オブジェクトへの代入で最もエラーを起こしやすいのは、新しい状態が必要とするリソース(メモリ、ディスクリプタなど)を確実に取得することです。

  • その取得は、新しい値のコピーが作成された場合にオブジェクト(つまり *this )の現在の状態を変更するに試行できます。これが rhs参照ではなく(つまりコピー)によって受け入れられる理由です。

  • ローカルコピーの状態をスワップする rhs*this は、ローカルコピーが後で特定の状態を必要としないことを考えると、通常、比較的簡単に潜在的な失敗/例外なしで実行できます(デストラクタを実行するのに適した状態が必要です)移動元のオブジェクト> = C ++ 11)

いつ使用すべきですか?(どの問題が[/ create]を解決しますか?)

  • 例外をスローする割り当ての影響を受けずに、割り当て先のオブジェクトにしたい場合は、強力な例外保証付きの swap があるか、または書き込めると想定し、失敗/ throw できないものが理想的です。†

  • (より単純な)コピーコンストラクター、 swap 関数、およびデストラクタ関数の観点から代入演算子を定義する、クリーンで理解しやすく、堅牢な方法が必要な場合。

    • コピーアンドスワップとして行われる自己割り当てにより、見過ごされがちなエッジケースが回避されます。‡

  • 割り当て中に余分な一時的なオブジェクトを持つことで生じるパフォーマンスの低下や一時的なリソース使用量の増加が、アプリケーションにとって重要ではない場合。⁂

swap スロー:一般に、オブジェクトがポインターで追跡するデータメンバーを確実にスワップできますが、スローフリースワップを持たない、またはスワップを X tmp = lhs; lhs = rhs; rhs = tmp; として実装する必要がある非ポインターデータメンバー; lhs = rhs; rhs = tmp; コピー構築または割り当てがスローされる可能性がありますが、一部のデータメンバーがスワップされたままになり、他のメンバーはスワップされないままになる可能性があります。この可能性は、Jamesが別の回答にコメントするC ++ 03 std::string も当てはまります。

@wilhelmtell:C ++ 03では、std :: string :: swap(std :: swapによって呼び出される)によってスローされる可能性のある例外についての言及はありません。 C ++ 0xでは、std :: string :: swapはnoexceptであり、例外をスローしてはなりません。 – James McNellis、2010年12月22日15:24


distinct個別のオブジェクトから割り当てるときに正気に見える割り当て演算子の実装は、自己割り当てが失敗する可能性があります。クライアントコードが自己割り当てを試みることさえ想像できないように思えるかもしれませんが、 x = f(x); して、コンテナーのアルゴ操作中に比較的簡単に発生する可能性があります。ここで、 f は(おそらく一部の #ifdef ブランチのみ)マクロala #define f(x) x または x への参照を返す関数、または x = c1 ? x * 2 : c2 ? x / 2 : x; )。例えば:

struct X
{
    T* p_;
    size_t size_;
    X& operator=(const X& rhs)
    {
        delete[] p_;  // OUCH!
        p_ = new T[size_ = rhs.size_];
        std::copy(p_, rhs.p_, rhs.p_ + rhs.size_);
    }
    ...
};

自己割り当てでは、上記のコードは x.p_; 削除します。、新しく割り当てられたヒープ領域で p_ をポイントし、その中で初期化されていないデータ(未定義の動作)を読み取ろうとします。それがあまりにも奇妙なことを行わない場合、 copy は破壊されたすべての 'T'に自己割り当てを試みます!


⁂コピーアンドスワップイディオムは、余分な一時変数を使用するため、非効率性または制限が生じる可能性があります(オペレーターのパラメーターがコピー構築される場合)。

struct Client
{
    IP_Address ip_address_;
    int socket_;
    X(const X& rhs)
      : ip_address_(rhs.ip_address_), socket_(connect(rhs.ip_address_))
    { }
};

ここで、手書きの Client::operator= は、 *thisrhs と同じサーバーにすでに接続されているかどうかをチェックする可能性があります(おそらく「リセット」コードを送信すると便利です)。一方、コピーアンドスワップアプローチでは、コピー別個のソケット接続を開き、元の接続を閉じるように作成される可能性が高いコンストラクター。これは、単純なインプロセス変数コピーの代わりにリモートネットワークの相互作用を意味するだけでなく、ソケットリソースまたは接続でクライアントまたはサーバーの制限に違反する可能性があります。 (もちろん、このクラスにはかなり恐ろしいインターフェースがありますが、それは別の問題です;-P)。




Answer 4 Oleksiy


この回答は、どちらかというと上記の回答に加筆・修正を加えたようなものです。

Visual Studioの一部のバージョン(および場合によっては他のコンパイラー)には、本当に煩わしいバグがあり、意味がありません。したがって、次のように swap 関数を宣言/定義すると、

friend void swap(A& first, A& second) {

    std::swap(first.size, second.size);
    std::swap(first.arr, second.arr);

}

... swap 関数を呼び出すと、コンパイラはあなたに怒鳴ります:

enter image description here

これは、 friend 関数が呼び出され、 this オブジェクトがパラメーターとして渡されることに関係しています。


これを回避する方法は、 friend キーワードを使用せずに swap 関数を再定義することです。

void swap(A& other) {

    std::swap(size, other.size);
    std::swap(arr, other.arr);

}

今回は、単に swap を呼び出して other を渡すことができるため、コンパイラーを満足させることができます。

enter image description here


結局のところ、2つのオブジェクトを交換するために friend 関数を使用する必要ありません。 other 1つのオブジェクトをパラメーターとして持つメンバー関数を swap するのと同じくらい意味があります。

this オブジェクトにはすでにアクセスしているため、パラメーターとして渡すことは技術的に冗長です。




Answer 5 Kerrek SB


C++11 スタイルのアロケータ対応コンテナを扱う際には、一言注意を加えておきたいと思います。スワッピングと代入は微妙に異なるセマンティクスを持っています。

具体的には、コンテナー std::vector<T, A> 考えてみましょう。ここで、 A はステートフルアロケータータイプであり、次の関数を比較します。

void fs(std::vector<T, A> & a, std::vector<T, A> & b)
{ 
    a.swap(b);
    b.clear(); // not important what you do with b
}

void fm(std::vector<T, A> & a, std::vector<T, A> & b)
{
    a = std::move(b);
}

両方の機能の目的 fsfm 与えることです状態 b が最初に持っていたし。ただし、隠れた質問があります a.get_allocator() != b.get_allocator() どうなりますか?答えは次のとおりです。 AT = std::allocator_traits<A> 書いてみましょう。 a

  • もし AT::propagate_on_container_move_assignment IS std::true_type 、その後 fm の再割り当てアロケータの値 b.get_allocator() は、それ以外の場合はない、と元のアロケータを使用し続けます。その場合、 ab のストレージには互換性がないため、データ要素を個別に交換する必要があります。 a a

  • もし AT::propagate_on_container_swap IS std::true_type 、その後、 fs はスワップ予想ファッションの両方のデータとアロケータを。

  • 場合は AT::propagate_on_container_swap ある std::false_type 、次に我々は、動的なチェックが必要です。

    • もし a.get_allocator() == b.get_allocator() 、2つの容器は、互換性のストレージを使用し、通常の方法で進むを交換。
    • ただし、 a.get_allocator() != b.get_allocator() 場合、プログラムは未定義の動作をします([container.requirements.general / 8]を参照)。

結果として、コンテナがステートフル アロケータのサポートを開始するとすぐに、C++11 ではスワップの操作が非自明なものになってしまいます。これはやや「高度なユースケース」ですが、まったく可能性がないわけではありません。なぜなら、移動の最適化は通常、クラスがリソースを管理するようになって初めて面白くなるからです。