このイディオムとは何ですか?いつ使用する必要がありますか?どの問題を解決しますか?C ++ 11を使用するとイディオムは変わりますか?
多くの場所で言及されていますが、「それは何か」という単一の質問と回答はありませんでした。これは以前に言及された場所の部分的なリストです:
このイディオムとは何ですか?いつ使用する必要がありますか?どの問題を解決しますか?C ++ 11を使用するとイディオムは変わりますか?
多くの場所で言及されていますが、「それは何か」という単一の質問と回答はありませんでした。これは以前に言及された場所の部分的なリストです:
回答:
リソースを管理するクラス(スマートポインターのようなラッパー)は、ビッグスリーを実装する必要があります。コピーコンストラクターとデストラクターの目標と実装は単純ですが、コピー代入演算子は間違いなく最も微妙で難しいものです。どのようにすればよいですか?どのような落とし穴を避ける必要がありますか?
コピーおよびスワップイディオムはソリューションであり、そしてエレガントに二つのことを達成するために、代入演算子を支援:回避コードの重複を、そして提供する強力な例外保証を。
概念的には、コピーコンストラクタの機能を使用してデータのローカルコピーを作成し、コピーしたデータをswap
関数で取得して、古いデータを新しいデータと交換します。次に、一時的なコピーが破棄され、古いデータがそれとともに取得されます。新しいデータのコピーが残ります。
コピーアンドスワップイディオムを使用するには、3つのものが必要です。有効なコピーコンストラクター、有効なデストラクタ(どちらもラッパーの基礎であり、いずれにしても完全である必要があります)、およびswap
関数です。
swap関数は、クラスの2つのオブジェクト(member for member)を交換する非スロー関数です。std::swap
独自のものを提供する代わりに使用したくなるかもしれませんが、これは不可能です。std::swap
実装内でコピーコンストラクターとコピー代入演算子を使用しており、最終的には代入演算子をそれ自体で定義しようとしています!
(それだけでなく、への修飾されていない呼び出し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;
}
そして、私たちは完成したと言います。これはリークなしでアレイを管理するようになりました。ただし、3つの問題があり、コード内で順次とマークされています(n)
。
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番目の問題、つまりコードの重複につながります。私たちの代入演算子は、既に他の場所で記述したすべてのコードを効果的に複製します。それはひどいことです。
私たちの場合、その中心はたった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 ++の次のバージョンである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);
}
// ...
};
何が起きてる?move-constructionの目標を思い出してください。クラスの別のインスタンスからリソースを取得し、割り当て可能で破壊可能であることが保証された状態のままにします。
したがって、私たちが行ったことは単純です。デフォルトのコンストラクター(C ++ 11機能)を介して初期化し、次にとスワップしother
ます。クラスのデフォルトで構築されたインスタンスを安全に割り当て、破棄other
できることがわかっているので、スワッピング後に同じことができるようになります。
(一部のコンパイラーはコンストラクターの委任をサポートしていないことに注意してください。この場合、手動でデフォルトでクラスを作成する必要があります。これは残念ながら幸運なことに簡単な作業です。)
これがクラスに加える必要がある唯一の変更です。なぜそれが機能するのですか?パラメータを参照ではなく値にするという重要な決定を思い出してください。
dumb_array& operator=(dumb_array other); // (1)
現在、other
が右辺値で初期化されている場合は、move-constructedになります。完璧です。C ++ 03が引数by-valueを使用してコピーコンストラクター機能を再利用できるように、C ++ 11 も適切な場合にmove-constructor を自動的に選択します。(そしてもちろん、以前にリンクされた記事で述べたように、値のコピー/移動は単に完全に省略されるかもしれません。)
そして、コピーアンドスワップのイディオムはこれで終わりです。
*なぜmArray
null に設定するのですか?演算子のコードがさらにスローされると、デストラクタdumb_array
が呼び出される可能性があるためです。nullに設定せずにそれが発生した場合、すでに削除されているメモリを削除しようとします!nullを削除することは操作ではないので、nullに設定することでこれを回避します。
†あり、我々は特化すべきであることを他の主張しているstd::swap
私たちのタイプについては、クラスを提供、swap
に沿って側フリー機能swap
などを、しかし、これはすべて不要です。任意の適切な使用は、swap
非修飾呼び出しを通してなり、私たちの機能は次のようになりますADLで見つかりました。1つの関数が行います。
‡理由は簡単です。リソースを自分に割り当てたら、必要な場所に入れ替えたり移動したりできます(C ++ 11)。また、パラメーターリストにコピーを作成することで、最適化を最大化できます。
††ムーブコンストラクターは通常である必要がありますnoexcept
。そうでない場合、一部のコード(たとえば、std::vector
サイズ変更ロジック)は、ムーブが意味をなす場合でもコピーコンストラクターを使用します。もちろん、内部のコードが例外をスローしない場合を除いて、それだけをマークしないでください。
swap
などboost::swap
、遭遇する最も一般的なコードでADLを機能させるには、ADLの実行中に検出される必要があります。スワップはC ++のトリッキーな問題であり、一般に、単一のアクセスポイントが(一貫性のために)最善であることに同意するようになりました。一般に、これを行う唯一の方法は、フリー関数です(int
スワップメンバーを持つことはできません)。例えば)。背景については、私の質問を参照してください。
基本的に、割り当ては2つのステップで行われます。オブジェクトの古い状態を破棄し、新しい状態を他のオブジェクトの状態のコピーとして構築します。
基本的に、それはデストラクタとコピーコンストラクタが行うことなので、最初のアイデアは作業をそれらに委任することです。ただし、破壊が失敗してはならないため、構築は可能ですが、実際には逆にしたいと思います。最初に構築部分を実行し、それが成功した場合は破壊部分を実行します。コピーアンドスワップイディオムは、まさにそれを行う方法です。まず、クラスのコピーコンストラクターを呼び出して一時オブジェクトを作成し、次にそのデータを一時オブジェクトと交換し、次に一時クラスのデストラクタに古い状態を破棄させます。
以来swap()
失敗しないことが想定されており、失敗する可能性がある唯一の部分はコピー構築です。それが最初に実行され、失敗した場合、ターゲットオブジェクトでは何も変更されません。
洗練された形式では、コピーとスワップは、代入演算子の(非参照)パラメーターを初期化してコピーを実行することによって実装されます。
T& operator=(T tmp)
{
this->swap(tmp);
return *this;
}
std::swap(this_string, that)
スロー禁止の保証はありません。強力な例外安全性を提供しますが、スロー禁止を保証しません。
std::string::swap
(によって呼び出されるstd::swap
)によってスローされる可能性のある例外についての言及はありません。C ++ 0xでは、std::string::swap
isはnoexcept
例外をスローしてはなりません。
std::array
ます...)
すでにいくつかの良い答えがあります。私は主にそれらが欠けていると思うものに焦点を当てます-コピーアンドスワップイディオムの「短所」の説明...
コピーアンドスワップイディオムとは何ですか?
スワップ関数の観点から代入演算子を実装する方法:
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;
コピー構築または割り当てとしてスワップを実装する必要がある非ポインターデータメンバースローする可能性がありますが、一部のデータメンバーがスワップされたままになり、他のメンバーはスワップされないままになる可能性があります。この可能性は、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=
かどうかを確認する場合*this
がありますrhs
(おそらく「リセット」コードを送信すると便利です)。一方、コピーアンドスワップアプローチでは、コピーコンストラクタが呼び出され、個別のソケット接続は元の接続を閉じます。これは、単純なインプロセス変数コピーの代わりにリモートネットワークの相互作用を意味するだけでなく、ソケットリソースまたは接続でクライアントまたはサーバーの制限に達してしまう可能性があります。(もちろん、このクラスには恐ろしいインターフェースがありますが、それは別の問題です;-P)。
Client
は、代入が禁止されていないことです。
この回答は、上記の回答に対する追加とわずかな変更に似ています。
Visual Studioの一部のバージョン(およびおそらく他のコンパイラー)には、本当に煩わしいバグがあり、意味がありません。したがって、次のswap
ように関数を宣言/定義すると、
friend void swap(A& first, A& second) {
std::swap(first.size, second.size);
std::swap(first.arr, second.arr);
}
... swap
関数を呼び出すと、コンパイラはあなたに怒鳴りつけます:
これは、friend
呼び出される関数とthis
パラメーターとして渡されるオブジェクトに関係しています。
これを回避する方法は、friend
キーワードを使用せずにswap
関数を再定義することです:
void swap(A& other) {
std::swap(size, other.size);
std::swap(arr, other.arr);
}
今回は、を呼び出しswap
て渡すだけother
でよいので、コンパイラーを満足させることができます。
結局のところ、2つのオブジェクトを交換するために関数を使用する必要はありませんfriend
。swap
1つのother
オブジェクトをパラメーターとして持つメンバー関数を作成するのと同じくらい意味があります。
すでにthis
オブジェクトにアクセスできるため、パラメーターとして渡すことは技術的に冗長です。
friend
関数が*this
パラメーター付きで呼び出されるたびにエラーが発生するようです
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);
}
両方の機能の目的fs
及びfm
提供することですa
状態b
最初に持っていたし。ただし、隠された質問がありa.get_allocator() != b.get_allocator()
ます。答えは次のとおりです。書いてみましょうAT = std::allocator_traits<A>
。
場合AT::propagate_on_container_move_assignment
でありstd::true_type
、その後fm
のアロケータ再割り当てa
の値をb.get_allocator()
、それ以外の場合はない、そして、a
元のアロケータを使用し続けます。その場合、a
とのストレージにb
互換性がないため、データ要素を個別に交換する必要があります。
場合AT::propagate_on_container_swap
で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()
、プログラムの動作は未定義です(cf. [container.requirements.general / 8]。その結果、コンテナーがステートフルアロケーターのサポートを開始するとすぐに、C ++ 11ではスワッピングが重要な操作になっています。これはやや「高度なユースケース」ですが、移動の最適化は通常、クラスがリソースを管理してはじめて興味深いものになり、メモリが最も人気のあるリソースの1つになるため、完全に可能性は低くありません。