[解決済み] 代入演算子と `if (this != &rhs)` の移動
質問
クラスの代入演算子では、通常、代入されるオブジェクトが呼び出したオブジェクトであるかどうかをチェックする必要があります。
Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}
return *this;
}
移動代入演算子にも同じものが必要なのか?という状況はありますか?
this == &rhs
が真になるのでしょうか?
? Class::operator=(Class&& rhs) {
?
}
どのように解決するのですか?
うわー、ここには掃除するものがたくさんある...。
まず
コピーとスワップ
は、必ずしもCopy Assignmentを実装するための正しい方法とは限りません。 ほぼ確実に
dumb_array
の場合、これは最適とは言えない解決策です。
の使用は
コピーとスワップ
は
dumb_array
は、最も充実した機能を持つ最も高価なオペレーションを最下層に置く典型的な例です。 これは、最も充実した機能を必要とし、パフォーマンス上のペナルティを支払うことをいとわないクライアントに最適です。 彼らはまさに欲しいものを手に入れることができるのです。
しかし、最も充実した機能を必要とせず、代わりに最高のパフォーマンスを求めているクライアントにとっては悲惨なことです。 彼らには
dumb_array
は、遅すぎるために書き換えなければならないソフトウェアの 1 つに過ぎません。 あった
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;
}
これは実は、賛否両論のある質問です。 ある人は絶対にイエスと言うでしょうし、ある人はノーと言うでしょう。
私の個人的な意見は、「いいえ、このチェックは必要ありません」です。
根拠は?
オブジェクトがrvalueの参照にバインドされるとき、それは2つのうちの1つです。
- 一時的なもの。
- 呼び出し元が一時的なものであると思わせたいオブジェクト。
実際のテンポラリであるオブジェクトへの参照を持っている場合、定義により、そのオブジェクトへのユニークな参照を持っていることになります。 プログラム全体の他のどこからも参照されることはあり得ません。 すなわち
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
の例では、これは単にアサー トを省略するか、あるいは move-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つ自己割り当てした場合
dumb_array
を自己割り当てした場合、プログラムに無駄な命令を挿入する以外には、何も間違ったことはしていません。 この同じ観察は、大半のオブジェクトに対して行うことができます。
<
更新
>
私はこの問題をもう少し考え、そして私の立場を多少変えました。 私は今、割り当てが自己割り当てを許容すべきであると信じていますが、コピー割り当てと移動割り当てのポスト条件は異なるものであると信じています。
コピー代入については
x = y;
の値は、ポストコンディションを持つ必要があります。
y
の値が変更されてはならない。 このとき
&x == &y
である場合、この後条件は次のように翻訳されます: 自己コピー割り当てが
x
.
移動割り当ての場合。
x = std::move(y);
というポストコンディションを用意する必要があります。
y
が有効だが不特定の状態であることを示す。 このとき
&x == &y
であれば、この後件を訳すと
x
は有効な、しかし特定されていない状態を持っている。 すなわち、自己の移動の割り当ては、no-opである必要はないのです。 しかし、クラッシュすべきではない。 この後処理条件は
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
を任意の有効な、しかし特定されない状態にすることができます。
の移動代入演算子をプログラムする方法は3つありますね。
dumb_array
の移動代入演算子をプログラムしてこれを実現する方法があります。
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;
}
上記の実装では、コピー代入演算子と同じように、no-opにすることで、自己代入を許容しています。 これも問題ありません。
dumb_array& operator=(dumb_array&& other)
{
swap(other);
return *this;
}
上記は、以下の場合のみOKです。
dumb_array
が破壊されるべきリソースを保持していない場合のみです。 例えば、唯一のリソースがメモリである場合、上記は問題ありません。 もし
dumb_array
がミューテックス ロックまたはファイルのオープン状態を保持する可能性がある場合、クライアントは移動割り当ての Lhs にあるこれらのリソースがすぐに解放されることを合理的に期待することができ、したがってこの実装は問題がある可能性があります。
最初のコストは、2 つの余分なストアです。 2 番目のコストは、テストと分岐です。 どちらも動作します。 両方とも、C++11 標準の表 22 MoveAssignable 要件のすべてを満たしています。 3 番目のものは、非メモリリソースに関する問題を修正しても動作します。
3 つの実装はすべて、ハードウェアによって異なるコストが発生する可能性があります:分岐はどのくらい高価か? 分岐はどのくらい高価なのか、たくさんのレジスタがあるのか、それとも非常に少ないのか?
要点は、自己移動割り当ては自己コピー割り当ては異なり、現在の値を保持する必要がないことです。
<
/更新
>
Luc Danton のコメントに触発されて、最後の (できれば) 編集を行いました。
もしあなたが、直接メモリを管理しない (しかし、管理するベースやメンバーを持つかもしれない) 高レベルのクラスを書いているなら、移動割り当ての最適な実装はしばしば
Class& operator=(Class&&) = default;
これは、各ベースと各メンバーを順番に割り当てていくものであり、各ベースには
this != &other
をチェックしません。 これは、ベースとメンバー間で不変量を維持する必要がないと仮定して、非常に高いパフォーマンスと基本的な例外安全性を提供します。 強力な例外安全性を必要とするクライアントには
strong_assign
.
関連
-
[解決済み】C-stringを使用すると警告が表示される。"ローカル変数に関連するスタックメモリのアドレスが返される"
-
[解決済み】致命的なエラー LNK1169: ゲームプログラミングで1つ以上の多重定義されたシンボルが発見された
-
[解決済み] [Solved] インクルードファイルが開けません。'stdio.h' - Visual Studio Community 2017 - C++ Error
-
[解決済み】指定範囲内の乱数で配列を埋める(C++)
-
[解決済み] C++11では、標準化されたメモリモデルが導入されました。その意味するところは?そして、C++プログラミングにどのような影響を与えるのでしょうか?
-
[解決済み] スマートポインターとは何ですか?
-
[解決済み] ムーブセマンティクスとは何ですか?
-
[解決済み] std::move()とは何ですか?また、どのような場合に使用するのですか?
-
[解決済み】C/C++の"-->"演算子とは何ですか?
-
[解決済み] Intel CPU の _mm_popcnt_u64 で、32 ビットのループカウンターを 64 ビットに置き換えると、パフォーマンスが著しく低下します。
最新
-
nginxです。[emerg] 0.0.0.0:80 への bind() に失敗しました (98: アドレスは既に使用中です)
-
htmlページでギリシャ文字を使うには
-
ピュアhtml+cssでの要素読み込み効果
-
純粋なhtml + cssで五輪を実現するサンプルコード
-
ナビゲーションバー・ドロップダウンメニューのHTML+CSSサンプルコード
-
タイピング効果を実現するピュアhtml+css
-
htmlの選択ボックスのプレースホルダー作成に関する質問
-
html css3 伸縮しない 画像表示効果
-
トップナビゲーションバーメニュー作成用HTML+CSS
-
html+css 実装 サイバーパンク風ボタン
おすすめ
-
[解決済み】 unsigned int vs. size_t
-
[解決済み】クラステンプレートの引数リストがない
-
[解決済み] エラーが発生する。ISO C++は型を持たない宣言を禁じています。
-
[解決済み】C++ - 解放されるポインタが割り当てられていないエラー
-
[解決済み] クラスにデフォルトコンストラクタが存在しない。
-
[解決済み】cc1plus:エラー:g++で認識されないコマンドラインオプション"-std=c++11"
-
[解決済み】エラー。switchステートメントでcaseラベルにジャンプする
-
[解決済み】1つ以上の多重定義されたシンボルが見つかる
-
[解決済み】C++ - ステートメントがオーバーロードされた関数のアドレスを解決できない。
-
[解決済み] コピーアンドスワップ慣用句とは?