web-dev-qa-db-ja.com

代入演算子と「if(this!=&rhs)」を移動します

クラスの代入演算子では、通常、割り当てられているオブジェクトが呼び出しオブジェクトであるかどうかを確認する必要があります。

Class& Class::operator=(const Class& rhs) {
    if (this != &rhs) {
        // do the assignment
    }

    return *this;
}

移動割り当て演算子にも同じものが必要ですか? this == &rhsは本当ですか?

? Class::operator=(Class&& rhs) {
    ?
}
115
Seth Carnegie

うわー、ここにはきれいにすることがたくさんあります...

まず、 Copy and Swap は、常にコピー割り当てを実装する正しい方法ではありません。ほぼ確実に_dumb_array_の場合、これは次善のソリューションです。

Copy and Swap の使用は_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つのいずれかです。

  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)everであることに同意しません。自分のコードで見つかった場合は、パフォーマンスのバグと見なして修正します。ただし、許可する場合は、swap(x, x)が移動元の値に対してself-move-assignemnetのみを実行することに注意してください。 _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つの移動元(空の)_dumb_array_を自己割り当てする場合、プログラムに不要な命令を挿入することを除いて、間違ったことは何もしません。これと同じ観察は、大多数のオブジェクトに対して行うことができます。

_<_ Update _>_

私はこの問題をもう少し考えて、自分の立場を少し変えました。私は今、割り当ては自己割り当てに寛容であるべきだと考えていますが、コピー割り当てと移動割り当ての投稿条件は異なります:

コピー割り当ての場合:

_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;
}
_

上記の実装では、コピー割り当て演算子と同じ方法で、no-opにすることで自己割り当てを許容します。これも結構です。

_dumb_array& operator=(dumb_array&& other)
{
    swap(other);
    return *this;
}
_

上記は、_dumb_array_が「即座に」破壊されるリソースを保持していない場合にのみ問題ありません。たとえば、唯一のリソースがメモリの場合、上記で問題ありません。 _dumb_array_がミューテックスロックまたはファイルのオープン状態を保持できる場合、クライアントは、move割り当てのlhs上のリソースがすぐに解放されると合理的に予想できるため、この実装に問題が生じる可能性があります。

最初のコストは2つの追加ストアです。 2番目のコストは、テストと分岐です。両方とも機能します。どちらも、C++ 11標準の表22 MoveAssignable要件のすべての要件を満たしています。 3番目は、non-memory-resource-concernを法として機能します。

3つの実装はすべて、ハードウェアに応じて異なるコストを持つことができます。ブランチはどれくらい高価ですか?多くのレジスタがありますか、それとも非常に少数ですか?

重要な点は、self-copy-assignmentとは異なり、self-move-assignmentは現在の値を保持する必要がないことです。

_<_/Update _>_

リュック・ダントンのコメントに触発された最後の(願わくば)編集:

メモリを直接管理しない(ただし、ベースまたはメンバーを持つ可能性のある)高レベルクラスを記述する場合、移動割り当ての最適な実装は次のとおりです。

_Class& operator=(Class&&) = default;
_

これにより、各ベースと各メンバーが順に割り当てられ、_this != &other_チェックが含まれなくなります。これにより、ベースとメンバー間で不変条件を維持する必要がないと仮定すると、最高のパフォーマンスと基本的な例外安全性が得られます。強力な例外安全性を要求するクライアントの場合、それらを_strong_assign_に向けます。

132
Howard Hinnant

まず、移動割り当て演算子の署名が間違っています。移動はソースオブジェクトからリソースを盗むため、ソースは非const r-value参照である必要があります。

_Class &Class::operator=( Class &&rhs ) {
    //...
    return *this;
}
_

まだ(non -constl-value参照を介して戻ることに注意してください。

どちらのタイプの直接割り当てについても、標準では自己割り当てをチェックするのではなく、自己割り当てがクラッシュアンドバーンを引き起こさないことを確認します。一般に、_x = x_またはy = std::move(y)呼び出しを明示的に行う人はいませんが、特に複数の関数を介したエイリアシングにより、_a = b_またはc = std::move(d)が自己割り当てになる場合があります。自己割り当ての明示的なチェック、つまり_this == &rhs_は、trueの場合に関数の内容をスキップすることで、自己割り当ての安全性を確保する1つの方法です。しかし、それは最悪の方法の1つです。これは、(一般的には)まれなケースを最適化する一方で、より一般的なケース(分岐やキャッシュミスの可能性がある)に対する反最適化であるためです。

これで、(少なくとも)オペランドの1つが直接一時オブジェクトである場合、自己割り当てシナリオを作成することはできません。一部の人々は、そのようなケースを想定して、そのためにコードを最適化し、想定が間違っている場合にコードが自殺的に愚かになるように主張しています。同じオブジェクトのチェックをユーザーにダンプするのは無責任だと言います。コピー割り当てについては、そのような議論はしません。移動割り当ての位置を逆にするのはなぜですか?

別の回答者から変更した例を作成しましょう:

_dumb_array& dumb_array::operator=(const dumb_array& other)
{
    if (mSize != other.mSize)
    {
        delete [] mArray;
        mArray = nullptr;  // clear this...
        mSize = 0u;        // ...and this in case the next line throws
        mArray = other.mSize ? new int[other.mSize] : nullptr;
        mSize = other.mSize;
    }
    std::copy(other.mArray, other.mArray + mSize, mArray);
    return *this;
}
_

このコピー割り当ては、明示的なチェックなしで自己割り当てを適切に処理します。コピー元とコピー先のサイズが異なる場合、コピーの前に割り当て解除と再割り当てが行われます。それ以外の場合は、コピーのみが行われます。自己割り当てでは、最適化されたパスは取得されず、ソースと宛先のサイズが等しくなると同じパスにダンプされます。 2つのオブジェクトが同じ場合(同じオブジェクトである場合を含む)、コピーは技術的に不要ですが、チェック自体が最も無駄に​​なるため、等価チェック(値またはアドレス)を行わない場合の価格です当時の。ここでのオブジェクトの自己割り当てにより、一連の要素レベルの自己割り当てが発生することに注意してください。要素タイプはこれを行うために安全でなければなりません。

ソースの例と同様に、このコピー割り当ては、基本的な例外の安全性保証を提供します。強力な保証が必要な場合は、コピー割り当てと移動割り当ての両方を処理する元の Copy and Swap クエリから統合割り当て演算子を使用します。ただし、この例のポイントは、速度を上げるために安全性を1ランク下げることです。 (ところで、個々の要素の値は独立していると仮定しています;他の要素と比較していくつかの値を制限する不変の制約はありません。)

この同じタイプの移動割り当てを見てみましょう。

_class dumb_array
{
    //...
    void swap(dumb_array& other) noexcept
    {
        // Just in case we add UDT members later
        using std::swap;

        // both members are built-in types -> never throw
        swap( this->mArray, other.mArray );
        swap( this->mSize, other.mSize );
    }

    dumb_array& operator=(dumb_array&& other) noexcept
    {
        this->swap( other );
        return *this;
    }
    //...
};

void  swap( dumb_array &l, dumb_array &r ) noexcept  { l.swap( r ); }
_

カスタマイズが必要なスワップ可能な型には、その型と同じ名前空間にswapという2つの引数のない関数が必要です。 (名前空間の制限により、スワップへの非修飾呼び出しが機能するようになります。)コンテナタイプは、標準コンテナに一致するパブリックswapメンバー関数も追加する必要があります。メンバーswapが提供されていない場合、おそらくフリー関数swapを交換可能なタイプのフレンドとしてマークする必要があります。 swapを使用するように移動をカスタマイズする場合、独自のスワッピングコードを提供する必要があります。標準コードは型の移動コードを呼び出します。これにより、移動カスタマイズ型の無限の相互再帰が発生します。

デストラクタと同様に、スワップ関数と移動操作は可能な限りスローせず、おそらくそのようにマークする必要があります(C++ 11)。標準ライブラリのタイプとルーチンには、スロー不可の移動タイプの最適化があります。

移動割り当てのこの最初のバージョンは、基本契約を満たします。ソースのリソースマーカーは、宛先オブジェクトに転送されます。ソースオブジェクトがそれらを管理するようになったため、古いリソースはリークされません。また、ソースオブジェクトは使用可能な状態のままになり、割り当てや破棄などのさらなる操作を適用できるようになります。

swap呼び出しがそうであるため、この移動割り当ては自己割り当てに対して自動的に安全であることに注意してください。また、非常に安全です。問題は、不必要なリソースの保持です。宛先の古いリソースは概念的には不要になりましたが、ここではまだソースオブジェクトが有効なままであるために残っています。スケジュールされたソースオブジェクトの破棄がかなり先の場合、リソーススペースを浪費しています。または、リソーススペースの合計が制限され、(新しい)ソースオブジェクトが正式に消滅する前に他のリソース要求が発生する場合はさらに悪化します。

この問題が、移動割り当て中のセルフターゲティングに関する物議を醸す現在の第一人者のアドバイスを引き起こしたものです。リソースを残さずに移動割り当てを記述する方法は次のようなものです。

_class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        delete [] this->mArray;  // kill old resources
        this->mArray = other.mArray;
        this->mSize = other.mSize;
        other.mArray = nullptr;  // reset source
        other.mSize = 0u;
        return *this;
    }
    //...
};
_

ソースはデフォルトの状態にリセットされ、古い宛先リソースは破棄されます。自己割り当ての場合、現在のオブジェクトは最終的に自殺します。それを回避する主な方法は、アクションコードをif(this != &other)ブロックで囲むか、それをねじ込んでクライアントにassert(this != &other)の最初の行を食べさせることです(ニースを感じている場合)。

もう1つの方法は、コピー割り当てを統合割り当てなしで強く安全な例外安全にする方法を研究し、それを移動割り当てに適用することです。

_class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        dumb_array  temp{ std::move(other) };

        this->swap( temp );
        return *this;
    }
    //...
};
_

otherthisが異なる場合、othertempへの移動によって空になり、そのままになります。次に、thistempに元々保持されていたリソースを取得しながら、otherに古いリソースを失います。そうすると、thisの古いリソースは、tempが削除すると削除されます。

自己割り当てが発生すると、othertempに空にすると、thisも空になります。 tempthisがスワップすると、ターゲットオブジェクトはそのリソースを取り戻します。 tempの死は空のオブジェクトを要求しますが、これは実質的にノーオペレーションです。 this/otherオブジェクトはリソースを保持します。

移動の構成とスワッピングも同じである限り、移動の割り当ては決してスローされるべきではありません。自己割り当ての際にも安全であるためのコストは、低レベルの型に対するいくつかの追加の命令であり、割り当て解除の呼び出しによって圧倒されるはずです。

11
CTMacUser

私は、自己割り当ての安全な演算子を望んでいますが、_operator=_の実装で自己割り当てチェックを書きたくないのです。そして実際、_operator=_を実装することすら望まないので、デフォルトの動作が「そのまま」動作することを望みます。最高の特別なメンバーは無料で来るものです。

そうは言っても、標準に存在するMoveAssignable要件は次のように説明されます(17.6.3.1テンプレート引数要件[utility.arg.requirements]、n3290から)。

式戻り値のタイプ戻り値事後条件
 t = rv T&t tは、割り当て前のrvの値と同等です

ここで、プレースホルダーは次のように記述されています。「t [is a] modifiable lvalue of type T;」 「rvはT型の右辺値です;」。これらは標準ライブラリのテンプレートの引数として使用されるタイプに課せられる要件であることに注意してください。しかし、標準の別の場所を見ると、ムーブ割り当てのすべての要件はこれに似ています。

これは、a = std::move(a)が「安全」でなければならないことを意味します。必要なものがIDテスト(たとえば_this != &other_)である場合は、それを実行します。そうしないと、オブジェクトを_std::vector_に入れることさえできなくなります。 (MoveAssignableを必要とするメンバー/操作を使用しない場合を除き、それを気にしないでください。)前の例a = std::move(a)では、_this == &other_が実際に保持されることに注意してください。

6
Luc Danton

現在のoperator=関数が記述されているため、右辺値参照引数constを作成したため、ポインターを「盗み」、着信右辺値参照の値を変更する方法はありません。 ..単に変更することはできず、読み取りのみが可能です。通常のlvaue-reference operator=メソッドの場合と同様に、deleteオブジェクトのポインターなどでthisの呼び出しを開始する場合にのみ問題が発生しますが、つまり、右辺バージョンを使用して、基本的にconst- lvalue operator=メソッドに残される同じ操作を行うのは冗長に思えます。

operator=const以外の右辺値参照を取るように定義した場合、必要なチェックを確認できる唯一の方法は、thisオブジェクトを一時的ではなく意図的に右辺値参照を返す関数。

たとえば、誰かがoperator+関数を記述し、右辺値参照と左辺値参照の組み合わせを利用して、オブジェクト型でのスタック加算操作中に余分な一時が作成されないようにします。

struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
          //of rhs and set the original pointers of rhs to NULL

A&& operator+(A& rhs, A&& lhs)
{
    //...code

    return std::move(rhs);
}

A&& operator+(A&& rhs, A&&lhs)
{
    //...code

    return std::move(rhs);
}

int main()
{
    A a;

    a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a

    //...rest of code
}

今、私は右辺値参照について理解していることから、上記を行うことは推奨されていません(つまり、右辺値ではなく一時的な値参照を返す必要があります)が、誰かがまだそれを行う場合は、チェックする必要があります着信rvalue-referenceがthisポインターと同じオブジェクトを参照していないことを確認してください。

2
Jason

私の答えは、移動割り当ては自己割り当てに対して保存する必要はありませんが、別の説明があります。 std :: unique_ptrを検討してください。実装する場合は、次のようにします。

_unique_ptr& operator=(unique_ptr&& x) {
  delete ptr_;
  ptr_ = x.ptr_;
  x.ptr_ = nullptr;
  return *this;
}
_

これを説明するスコット・マイヤーズ を見ると、彼は何か似たようなことをしています。 (スワップしない理由をさまよう場合-余分な書き込みが1つあります)。そして、これは自己割り当てにとって安全ではありません。

時々これは残念です。すべての偶数をベクトルから移動することを検討してください。

_src.erase(
  std::partition_copy(src.begin(), src.end(),
                      src.begin(),
                      std::back_inserter(even),
                      [](int num) { return num % 2; }
                      ).first,
  src.end());
_

これは整数では問題ありませんが、このような動作をムーブセマンティクスで行えるとは思いません。

結論として、オブジェクト自体への割り当ての移動は適切ではないため、注意が必要です。

小さなアップデート。

  1. 私はハワードに反対しますが、これは悪い考えですが、それでもなお、swap(x, x)は機能するはずなので、「移動した」オブジェクトの自己移動割り当てが機能するはずです。アルゴリズムはこれらのものが大好きです!コーナーケースが正常に機能する場合は、常に素晴らしいです。 (そして、私はまだ無料ではない場合を見ていません。しかし、それが存在しないわけではありません)。
  2. これは、unique_ptrsの割り当てがlibc ++でどのように実装されるかです。unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}自己移動割り当てに対して安全です。
  3. コアガイドライン 自己移動割り当ては問題ないはずです。
1

(これ== rhs)私が考えることができる状況があります。このステートメントの場合:Myclass obj; std :: move(obj)= std :: move(obj)

0
little_monster