web-dev-qa-db-ja.com

コピーアンドスワップイディオムとは何ですか?

この慣用句は何であり、いつ使用すべきですか?どの問題が解決しますか? C++ 11を使用したときの慣用句は変わりますか?

それは多くの場所で言及されていますが、私たちは「それは何ですか」という質問と答えを持っていませんでしたので、ここでそれはそうです。これは以前に言及された場所の部分的なリストです:

1822
GManNickG

概要

コピーアンドスワップイディオムが必要なのはなぜですか?

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

copy-and-swapイディオムは解決策であり、 コードの重複 の回避、および 強力な例外保証

どのように機能しますか?

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

コピーアンドスワップイディオムを使用するには、3つのものが必要です。動作するコピーコンストラクター、動作するデストラクター(いずれもラッパーの基礎であるため、とにかく完成する必要があります)、およびswap関数。

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

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


詳細な説明

目標

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

#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. 最初は自己割り当てテストです。このチェックには2つの目的があります:自己割り当てで不必要なコードを実行するのを防ぐ簡単な方法であり、微妙なバグ(試してコピーするためだけにアレイを削除するなど)から保護します。しかし、それ以外の場合はすべて、プログラムの速度を低下させ、コードのノイズとして機能するだけです。自己割り当てはめったに行われないため、ほとんどの場合、このチェックは無駄です。オペレーターがそれなしで適切に作業できればより良いでしょう。

  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関数。 The Rule of Threeはコピーコンストラクター、代入演算子、デストラクタの存在を必要としますが、実際には「The Big Three and A Half」と呼ばれる必要があります。クラスがリソースを管理するときはいつでも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);
    }

    // ...
};

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

苦労せずに、代入演算子は次のとおりです。

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

    return *this;
}

以上です! 1つの急降下で、3つの問題すべてが一度にエレガントに対処されます。

なぜ機能するのですか?

最初に重要な選択に気付きます:パラメータ引数は値ごとに取られます。次のことも同様に簡単に実行できます(実際、多くの単純なイディオムの実装も実行します)。

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)
        : dumb_array() // initialize via default constructor, C++11 only
    {
        swap(*this, other);
    }

    // ...
};

何が起きてる? move-constructionの目標を思い出してください。リソースをクラスの別のインスタンスから取得し、割り当て可能かつ破壊可能であることが保証された状態のままにすること。

したがって、私たちが行ったことは簡単です。デフォルトのコンストラクター(C++ 11の機能)で初期化してから、otherと交換します。クラスのデフォルトの構築済みインスタンスは安全に割り当ておよび破棄できることがわかっているので、otherがスワップ後に同じことを行えることがわかります。

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

なぜそれが機能するのですか?

それがクラスに加える必要がある唯一の変更であるため、なぜ機能するのでしょうか?パラメーターを参照ではなく値にするために行った重要な決定を思い出してください。

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

現在、otherが右辺値で初期化されている場合、それはmove-constructedになります。パーフェクト。同様に、C++ 03は引数ごとに値を取ることでコピーコンストラクター機能を再利用できます。C++ 11は、必要に応じて、移動コンストラクターをautomatically選びます。まあ。 (もちろん、以前にリンクされた記事で述べたように、値のコピー/移動は完全に省略される可能性があります。)

そして、コピーアンドスワップイディオムを締めくくります。


脚注

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

†私たちのタイプにstd::swapを特化し、フリー関数swapと一緒にクラス内でswapを提供するなど、他の主張があります。しかし、これはすべて不要です。swapの適切な使用は修飾されていない呼び出し。関数は ADL を介して検出されます。 1つの機能で十分です。

‡理由は簡単です。自分自身にリソースを取得したら、必要な場所でリソースを交換したり移動したりできます(C++ 11)。また、パラメーターリストにコピーを作成することにより、最適化を最大限に行うことができます。

2050
GManNickG

代入は、その中心となる2つのステップです。オブジェクトの古い状態コピーとして新しい状態の構築 _他のオブジェクトの状態。

基本的に、それがdestructorcopy constructorが行うことなので、最初のアイデアは作業をそれらに委任することです。しかし、破壊は失敗してはならないので、構築は可能かもしれませんがを逆にして実際にやりたいと思います:最初に構成部分を実行し、成功すればそれから破壊的な部分を行う。これは、まずクラスのコピーコンストラクタを呼び出してテンポラリを作成し、次にそのデータをテンポラリのものと交換してから、テンポラリのデストラクタに古い状態を破棄させることです。
swap()は決して失敗しないはずなので、失敗する可能性があるのはコピー構築だけです。それが最初に実行され、失敗した場合、ターゲットオブジェクトには何も変更されません。

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

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

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

コピーアンドスワップイディオムとは何ですか?

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

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

基本的な考え方は次のとおりです。

  • オブジェクトへの割り当てで最もエラーが発生しやすい部分は、新しい状態が必要とするリソース(メモリ、記述子など)を取得することです。

  • before新しい値のコピーが作成された場合にオブジェクトの現在の状態(つまり*this)を変更すると、その取得を試みることができます。これがrhsname__の理由ですby referenceではなく、by value(コピー)

  • ローカルコピーの状態を入れ替えるrhsname__と*thisusuallyローカルコピーが特別なものを必要としないため、潜在的な障害/例外なしに比較的簡単に実行できます。その後の状態(オブジェクトがmovedfrom> = C++ 11であるのと同様に、実行するデストラクタに適切な状態が必要です)

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

  • 強い例外保証付きのswapname__を持っているか、または書くことができると仮定し、理想的には失敗しない/throwname__..†を割り当てられたオブジェクトが例外をスローする割り当ての影響を受けないようにする場合

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

    • コピーアンドスワップとして行われる自己割り当てにより、見落とされがちなEdgeのケースを回避できます。‡
  • 割り当て中に余分な一時オブジェクトを使用することによってパフォーマンスが低下したり、一時的にリソース使用量が増えたりすることが、アプリケーションにとって重要ではない場合。 ⁂

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

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


‡別個のオブジェクトから割り当てるときに正常に思える割り当て演算子の実装は、自己割り当てで簡単に失敗する可能性があります。クライアントコードが自己割り当てを試みることさえ想像できないかもしれませんが、x = f(x);コードでfname__が(おそらくいくつかの#ifdefブランチのみ)マクロである場合、コンテナーのアルゴリズム操作中に比較的簡単に発生する可能性がありますala #define f(x) xまたはxname__への参照を返す関数、または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_を新しく割り当てられたヒープ領域にポイントしてから、その中のuninitialisedデータの読み取りを試みます。 (未定義の動作)、それがあまりにも奇妙なことをしない場合、copyname__は破壊されたばかりの「T」すべてに自己割り当てを試みます!


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

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

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

38
Tony Delroy

この答えは、上記の答えに対する追加とわずかな変更に似ています。

一部のバージョンの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


結局、friend関数を使って2つのオブジェクトを交換するのに need は必要ありません。 swapを、1つのotherオブジェクトをパラメータとして持つメンバ関数にするのも同様に意味があります。

あなたは既にthisオブジェクトにアクセスできるので、それをパラメータとして渡すことは技術的に冗長です。

22
Oleksiy

C++ 11スタイルのアロケータ対応コンテナを扱っているときには、Word of warningを追加したいと思います。スワップと代入は微妙に異なる意味を持ちます。

具体的には、Aがステートフルアロケータ型であるコンテナstd::vector<T, 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の目的は、abが最初に持っていた状態を与えることです。しかし、隠された質問があります:a.get_allocator() != b.get_allocator()ならどうなるでしょうか?答えは次のとおりです。それは異なります。 AT = std::allocator_traits<A>を書きましょう。

  • AT::propagate_on_container_move_assignmentstd::true_typeの場合、fmaのアロケーターにb.get_allocator()の値を再割り当てします。それ以外の場合はそうではなく、aは元のアロケーターを使用し続けます。その場合、abの格納には互換性がないため、データ要素を個別に交換する必要があります。

  • AT::propagate_on_container_swapstd::true_typeの場合、fsはデータとアロケータの両方を期待通りに交換します。

  • AT::propagate_on_container_swapstd::false_typeであれば、動的チェックが必要です。

    • a.get_allocator() == b.get_allocator()の場合、2つのコンテナは互換性のあるストレージを使用し、スワップは通常の方法で行われます。
    • ただし、a.get_allocator() != b.get_allocator()の場合、プログラムは 未定義の動作 (cf. [container.requirements.general/8]を参照してください。

つまり、C++ 11では、コンテナがステートフルアロケータのサポートを開始するとすぐに、スワップが重要な操作になりました。これはやや「高度なユースケース」ですが、移動の最適化は通常、クラスがリソースを管理するようになって初めて面白くなり、メモリが最も人気のあるリソースの1つになるため、まったく考えられません。

13
Kerrek SB