Copy&Swap イディオムを使用することで、強力な例外安全性を備えたコピー割り当てを簡単に実装できます。
T& operator = (T other){
using std::swap;
swap(*this, other);
return *this;
}
ただし、これにはT
が Swappable である必要があります。 std::is_move_constructible_v<T> && std::is_move_assignable_v<T> == true
のおかげでstd::swap
の場合、自動的にどのタイプになりますか。
私の質問は、代わりに「コピー&移動」イディオムを使用することの欠点はありますか?そのようです:
T& operator = (T other){
*this = std::move(other);
return *this;
}
T
のムーブ代入を実装する場合、それ以外の場合は明らかに無限再帰になるためです。
この質問は コピーアンドスワップイディオムはC++ 11でコピーアンドムーブイディオムになるべきですか? とは異なり、この質問はより一般的であり、実際の代わりにムーブ代入演算子を使用しますメンバーを手動で移動します。これにより、リンクされたスレッドで回答を予測したクリーンアップの問題が回避されます。
Copy&Moveを実装する方法は、@ Raxvanが指摘したとおりでなければなりません。
_T& operator=(const T& other){
*this = T(other);
return *this;
}
_
ただし、T(other)
はすでに右辺値であるため_std::move
_がないと、ここで_std::move
_を使用すると、clangはペシミゼーションに関する警告を発します。
ムーブ代入演算子が存在する場合、コピー&スワップとコピー&ムーブの違いは、ユーザーがムーブ代入よりも優れた例外安全性を持つswap
メソッドを使用しているかどうかによって異なります。標準の_std::swap
_の場合、例外の安全性はコピーとスワップとコピーと移動で同じです。ほとんどの場合、swap
とムーブ代入は同じ例外安全性を持っていると思います(常にではありません)。
コピー&ムーブを実装すると、ムーブ代入演算子が存在しないか、間違った署名がある場合、コピー代入演算子が無限再帰に減少するリスクがあります。ただし、少なくともclangはこれについて警告し、コンパイラに_-Werror=infinite-recursion
_を渡すことで、この恐れを取り除くことができます。これは率直に言って、デフォルトでエラーではない理由を超えていますが、私は逸脱します。
私はいくつかのテストとたくさんの頭の引っかき傷をしました、そしてここに私が見つけたものがあります:
ムーブ代入演算子がある場合、operator=(T)
の呼び出しがoperator=(T&&)
とあいまいであるため、コピーとスワップを実行する「適切な」方法は機能しません。 @Raxvanが指摘したように、コピー代入演算子の本体の内部でコピー構築を行う必要があります。これは、演算子が右辺値で呼び出されたときにコンパイラがコピーの省略を実行できないため、劣っていると見なされます。ただし、コピーの省略が適用された場合は、ムーブ代入によって処理されるため、ポイントは重要ではありません。
比較する必要があります:
_T& operator=(const T& other){
using std::swap;
swap(*this, T(other));
return *this;
}
_
に:
_T& operator=(const T& other){
*this = T(other);
return *this;
}
_
ユーザーがカスタムswap
を使用していない場合は、テンプレート化されたstd::swap(a,b)
が使用されます。これは本質的にこれを行います:
_template<typename T>
void swap(T& a, T& b){
T c(std::move(a));
a = std::move(b);
b = std::move(c);
}
_
つまり、コピー&スワップの例外安全性は、ムーブ構築とムーブ代入の弱い方と同じ例外安全性です。ユーザーがカスタムスワップを使用している場合、もちろん例外の安全性はそのスワップ機能によって決定されます。
コピー&ムーブでは、例外の安全性はムーブ代入演算子によって完全に決定されます。
ここでパフォーマンスを見るのは、コンパイラの最適化によってほとんどの場合違いがない可能性が高いため、一種の議論の余地があると思います。しかし、とにかく、コピーとスワップはコピー構築、移動構築、および2つの移動割り当てを実行しますが、コピー構築と1つの移動割り当てのみを実行するコピーと移動とは異なります。ほとんどの場合、コンパイラが同じマシンコードをクランクアウトすることを期待していますが、もちろんTによって異なります。
_ class T {
public:
T() = default;
T(const std::string& n) : name(n) {}
T(const T& other) = default;
#if 0
// Normal Copy & Swap.
//
// Requires this to be Swappable and copy constructible.
//
// Strong exception safety if `std::is_nothrow_swappable_v<T> == true` or user provided
// swap has strong exception safety. Note that if `std::is_nothrow_move_assignable` and
// `std::is_nothrow_move_constructible` are both true, then `std::is_nothrow_swappable`
// is also true but it does not hold that if either of the above are true that T is not
// nothrow swappable as the user may have provided a specialized swap.
//
// Doesn't work in presence of a move assignment operator as T t1 = std::move(t2) becomes
// ambiguous.
T& operator=(T other) {
using std::swap;
swap(*this, other);
return *this;
}
#endif
#if 0
// Copy & Swap in presence of copy-assignment.
//
// Requries this to be Swappable and copy constructible.
//
// Same exception safety as the normal Copy & Swap.
//
// Usually considered inferor to normal Copy & Swap as the compiler now cannot perform
// copy elision when called with an rvalue. However in the presence of a move assignment
// this is moot as any rvalue will bind to the move-assignment instead.
T& operator=(const T& other) {
using std::swap;
swap(*this, T(other));
return *this;
}
#endif
#if 1
// Copy & Move
//
// Requires move-assignment to be implemented and this to be copy constructible.
//
// Exception safety, same as move assignment operator.
//
// If move assignment is not implemented, the assignment to this in the body
// will bind to this function and an infinite recursion will follow.
T& operator=(const T& other) {
// Clang emits the following if a user or default defined move operator is not present.
// > "warning: all paths through this function will call itself [-Winfinite-recursion]"
// I recommend "-Werror=infinite-recursion" or "-Werror" compiler flags to turn this into an
// error.
// This assert will not protect against missing move-assignment operator.
static_assert(std::is_move_assignable<T>::value, "Must be move assignable!");
// Note that the following will cause clang to emit:
// warning: moving a temporary object prevents copy elision [-Wpessimizing-move]
// *this = std::move(T{other});
// The move doesn't do anything anyway so write it like this;
*this = T(other);
return *this;
}
#endif
#if 1
T& operator=(T&& other) {
// This will cause infinite loop if user defined swap is not defined or findable by ADL
// as the templated std::swap will use move assignment.
// using std::swap;
// swap(*this, other);
name = std::move(other.name);
return *this;
}
#endif
private:
std::string name;
};
_
私の質問は、代わりに「コピー&移動」イディオムを使用することの欠点はありますか?
はい、ムーブ代入operator =(T&&)
を実装しないと、スタックオーバーフローが発生します。それを実装したい場合は、コンパイラエラーが発生します( ここの例 ):
_struct test
{
test() = default;
test(const test &) = default;
test & operator = (test t)
{
(*this) = std::move(t);
return (*this);
}
test & operator = (test &&)
{
return (*this);
}
};
_
_test a,b; a = b;
_を実行すると、エラーが発生します。
error: ambiguous overload for 'operator=' (operand types are 'test' and 'std::remove_reference<test&>::type {aka test}')
これを解決する1つの方法は、コピーコンストラクターを使用することです。
_test & operator = (const test& t)
{
*this = std::move(test(t));
return *this;
}
_
これは機能しますが、ムーブ代入を実装しない場合、エラーが発生しない可能性があります(コンパイラの設定によって異なります)。人為的エラーを考慮すると、このケースが発生し、実行時にスタックオーバーフローが発生する可能性があります。これは悪いことです。