うわー、ここで片付けることがたくさんあります...
まず、コピーとスワップは、常にコピー割り当てを実装する正しい方法ではありません。ほぼ間違いなくの場合dumb_array
、これは次善のソリューションです。
使用コピーとスワップのためにあるdumb_array
最下層に最大限の機能を備えた最も高価な操作を置くの典型的な例です。これは、最大限の機能を必要とし、パフォーマンスのペナルティを支払う意思があるクライアントに最適です。彼らはまさに彼らが望むものを手に入れます。
しかし、完全な機能を必要とせず、代わりに最高のパフォーマンスを求めているクライアントにとっては悲惨です。彼らにとってdumb_array
、それは遅すぎるので彼らが書き直さなければならないもう一つのソフトウェアです。持っていたdumb_array
異なる設計されて、それがいずれかのクライアントへの妥協なしで両方のクライアントを満足している可能性があります。
両方のクライアントを満足させるための鍵は、最下位レベルで最速のオペレーションを構築し、その上にAPIを追加して、より多くの費用でより完全な機能を実現することです。つまり、強力な例外保証が必要です。罰金を払ってください。必要ないの?これはより速い解決策です。
具体的に見てみましょう:以下は、高速で基本的な例外保証の代入代入演算子ですdumb_array
。
dumb_array& operator=(const dumb_array& other)
{
if (this != &other)
{
if (mSize != other.mSize)
{
delete [] mArray;
mArray = nullptr;
mArray = other.mSize ? new int[other.mSize] : nullptr;
mSize = other.mSize;
}
std::copy(other.mArray, other.mArray + mSize, mArray);
}
return *this;
}
説明:
最新のハードウェアで実行できるより高価なことの1つは、ヒープにアクセスすることです。ヒープへの移動を回避するためにできることはすべて、時間と労力を費やすことです。のクライアントはdumb_array
、しばしば同じサイズの配列を割り当てたいと思うかもしれません。そして、彼らがそうするとき、あなたがする必要があるのはmemcpy
(の下に隠されているstd::copy
)です。同じサイズの新しい配列を割り当ててから、同じサイズの古い配列の割り当てを解除する必要はありません。
強力な例外安全を実際に必要とするクライアントのために:
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
swap(lhs, rhs);
return lhs;
}
または、C ++ 11の移動割り当てを利用したい場合は、次のようにする必要があります。
template <class C>
C&
strong_assign(C& lhs, C rhs)
{
lhs = std::move(rhs);
return lhs;
}
dumb_array
クライアントが速度を重視する場合は、を呼び出す必要がありoperator=
ます。強力な例外安全性が必要な場合は、さまざまなオブジェクトで機能し、一度だけ実装する必要がある、呼び出し可能な汎用アルゴリズムがあります。
ここで、元の質問に戻ります(現時点ではtype-oです)。
Class&
Class::operator=(Class&& rhs)
{
if (this == &rhs) // is this check needed?
{
// ...
}
return *this;
}
これは実際には物議を醸す問題です。はい、絶対に言う人もいれば、いいえと言う人もいます。
私の個人的な見解は「いいえ」です。このチェックは必要ありません。
根拠:
オブジェクトが右辺値参照にバインドするとき、それは次の2つのいずれかです。
- 一時的なもの。
- 発信者があなたに信じてほしいオブジェクトは一時的なものです。
実際の一時的なオブジェクトへの参照がある場合、定義により、そのオブジェクトへの一意の参照があります。プログラム全体のどこからでも参照することはできません。すなわちthis == &temporary
不可能です。
クライアントがあなたに嘘をつき、あなたがそうでないときに一時的なものになると約束した場合、あなたが気にする必要がないことを確認するのはクライアントの責任です。あなたが本当に注意したいのであれば、私はこれがより良い実装になると信じています:
Class&
Class::operator=(Class&& other)
{
assert(this != &other);
// ...
return *this;
}
つまり、自己参照が渡された場合、これは修正すべきクライアント側のバグです。
完全を期すために、次の移動代入演算子を示しdumb_array
ます。
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
移動割り当ての一般的な使用例では、移動元*this
オブジェクトになるためdelete [] mArray;
、何もしないでください。実装がnullptrをできるだけ速く削除することが重要です。
警告:
それswap(x, x)
は良い考えだとか、単に必要な悪だと主張する人もいます。そして、これがスワップがデフォルトのスワップになった場合、自己移動割り当てを引き起こす可能性があります。
私は反対しswap(x, x)
ている、これまで良いアイデア。自分のコードで見つかった場合は、パフォーマンスのバグと見なして修正します。ただし、許可する場合swap(x, x)
は、移動元の値に対してのみ自己移動割り当てを行うことを認識してください。また、このdumb_array
例では、アサートを省略するか、移動元のケースに制限するだけで、これは完全に無害です。
dumb_array& operator=(dumb_array&& other)
{
assert(this != &other || mSize == 0);
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
2つのmoved-from(empty)を自分で割り当てた場合dumb_array
、プログラムに無用な命令を挿入する以外は、何も問題はありません。この同じ観察は、大多数のオブジェクトに対して行うことができます。
<
更新>
この問題についてもう少し考え、私の立場を少し変えました。今では、割り当ては自己割り当てに寛容である必要があると考えていますが、コピー割り当てと移動割り当てのポスト条件は異なります。
コピー割り当ての場合:
x = y;
の値をy
変更してはならないという事後条件が必要です。そのとき&x == &y
、この事後条件は次のように変換されます。自己コピーの割り当てはの値に影響を与えませんx
。
移動割り当ての場合:
x = std::move(y);
y
有効であるが指定されていない状態を持つ事後条件が必要です。そのとき&x == &y
、この事後条件は次のように変換されますx
。有効ですが指定されていない状態です。つまり、自己移動割り当ては何もする必要はありません。しかし、クラッシュしないはずです。この事後条件はswap(x, x)
、単に動作できるようにすることと一致しています。
template <class T>
void
swap(T& x, T& y)
{
// assume &x == &y
T tmp(std::move(x));
// x and y now have a valid but unspecified state
x = std::move(y);
// x and y still have a valid but unspecified state
y = std::move(tmp);
// x and y have the value of tmp, which is the value they had on entry
}
上記が機能する限り、 x = std::move(x)
クラッシュしないします。x
有効だが不特定の状態のままになる可能性があります。
dumb_array
これを達成するために、代入代入演算子をプログラムする3つの方法があります。
dumb_array& operator=(dumb_array&& other)
{
delete [] mArray;
// set *this to a valid state before continuing
mSize = 0;
mArray = nullptr;
// *this is now in a valid state, continue with move assignment
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
return *this;
}
上記の実装では、自己の割り当てを許容するが、*this
およびother
自己移動割り当て後のゼロサイズのアレイの元の値がどのように関係なくなってしまう*this
です。これで結構です。
dumb_array& operator=(dumb_array&& other)
{
if (this != &other)
{
delete [] mArray;
mSize = other.mSize;
mArray = other.mArray;
other.mSize = 0;
other.mArray = nullptr;
}
return *this;
}
上記の実装では、コピー代入演算子を操作なしにすることで、コピー代入演算子と同じように自己代入を許容します。これも結構です。
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
上記は大丈夫です dumb_array
「即時」に破棄する必要があるリソースをが保持していないません。たとえば、唯一のリソースがメモリの場合、上記で問題ありません。もしdumb_array
ミューテックスロックまたはファイルのオープン状態を保持できる可能性がある、クライアントは移動割り当てのlh上のこれらのリソースがすぐに解放されることを合理的に期待できるため、この実装には問題がある可能性があります。
最初のコストは2つの追加のストアです。2番目のコストは、テストと分岐です。どちらも機能します。どちらも、C ++ 11標準の表22 MoveAssignable要件のすべての要件を満たしています。3番目は、メモリリソース以外の問題を法として機能します。
3つの実装はすべて、ハードウェアに応じて異なるコストがかかる可能性があります。レジスターはたくさんありますか、それともごくわずかですか?
持ち帰りは、自己コピー割り当てとは異なり、自己移動割り当ては現在の値を保持する必要がないということです。
<
/更新>
Luc Dantonのコメントに触発された最後の(うまくいけば)編集:
メモリを直接管理しない(ただし、ベースまたはメンバーが管理する可能性がある)高レベルのクラスを作成している場合、移動割り当ての最適な実装は次のとおりです。
Class& operator=(Class&&) = default;
これにより、各ベースと各メンバーが順番に割り当てられ、this != &other
チェックは含まれません。これにより、ベースとメンバー間で不変条件を維持する必要がないと仮定すると、最高のパフォーマンスと基本的な例外の安全性が得られます。強力な例外安全性を要求するクライアントの場合、それらをに向けstrong_assign
ます。