うわー、ここで掃除することがたくさんあります...
まず、コピーとスワップは、コピー割り当てを実装するための正しい方法であるとは限りません。の場合、ほぼ確実にdumb_array
、これは次善のソリューションです。
Copy と Swapの使用は、dumb_array
最もコストのかかる操作を最も完全な機能を備えた最下層に配置する典型的な例です。最大限の機能を必要とし、パフォーマンスの低下を喜んで支払うクライアントに最適です。彼らはまさに彼らが望むものを手に入れます。
しかし、完全な機能を必要とせず、代わりに最高のパフォーマンスを求めているクライアントにとっては悲惨です。彼らにとってdumb_array
は、遅すぎるために書き直さなければならないもう 1 つのソフトウェアにすぎません。別の方法で設計されていれdumb_array
ば、どちらのクライアントにも妥協することなく、両方のクライアントを満足させることができたはずです.
両方のクライアントを満足させるための鍵は、最も低いレベルで最速の操作を構築し、その上に API を追加して、より多くの費用をかけてより完全な機能を提供することです。つまり、強力な例外保証が必要です。それは有料です。必要ない?これがより速い解決策です。
具体的に説明しましょう: 高速で基本的な例外保証の Copy Assignment 演算子は次の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=
ます。強力な例外安全性が必要な場合は、さまざまなオブジェクトで機能し、1 回実装するだけで呼び出せる汎用アルゴリズムがあります。
元の質問に戻ります (この時点でタイプ 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)
、移動元の値に対してのみ self-move-assignemnet を実行することに注意してください。そして、このdumb_array
例では、単に assert を省略したり、moved-from ケースに制約したりすれば、これは完全に無害です。
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 (空の) を自己割り当てした場合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
、クライアントは、移動割り当ての左側にあるリソースがすぐに解放されることを合理的に期待できるため、この実装には問題が生じる可能性があります。
最初の費用は 2 つの余分なストアです。2 番目のコストは、テストと分岐です。どちらも機能します。どちらも、C++11 標準の表 22 MoveAssignable 要件のすべての要件を満たしています。3 つ目も、メモリ リソース以外の懸念に基づいて機能します。
3 つの実装はすべて、ハードウェアに応じてコストが異なる場合があります。ブランチのコストはどれくらいですか? レジスターはたくさんありますか、それともほとんどありませんか?
要点は、self-copy-assignment とは異なり、self-move-assignment は現在の値を保持する必要がないということです。
<
/アップデート>
Luc Danton のコメントに触発された最後の (できれば) 編集:
メモリを直接管理しない高レベルのクラスを作成している場合 (ただし、メモリを直接管理するベースまたはメンバーが含まれている可能性があります)、move 代入の最適な実装は多くの場合次のとおりです。
Class& operator=(Class&&) = default;
これにより、各ベースと各メンバーが順番に割り当てられ、this != &other
チェックは含まれません。これにより、ベースとメンバー間で不変条件を維持する必要がないと仮定すると、最高のパフォーマンスと基本的な例外安全性が得られます。強力な例外安全性を要求するクライアントの場合は、strong_assign
.