1. ホーム
  2. c++

[解決済み] 代入演算子と `if (this != &rhs)` の移動

2022-06-28 04:01:45

質問

クラスの代入演算子では、通常、代入されるオブジェクトが呼び出したオブジェクトであるかどうかをチェックする必要があります。

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つです。

  1. 一時的なもの。
  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 の例では、これは単にアサー トを省略するか、あるいは 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;
}

上記の実装では、自己代入を許容していますが *thisother の元の値が何であれ、自己移動の代入の後は結局ゼロサイズの配列になります。 *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 .