137

クラスの割り当て演算子では、通常、割り当てられているオブジェクトが呼び出し元のオブジェクトであるかどうかを確認する必要があるため、混乱を招くことはありません。

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

    return *this;
}

移動代入演算子にも同じことが必要ですか? 真になる状況はthis == &rhsありますか?

? Class::operator=(Class&& rhs) {
    ?
}
4

6 に答える 6

155

うわー、ここで掃除することがたくさんあります...

まず、コピーとスワップは、コピー割り当てを実装するための正しい方法であるとは限りません。の場合、ほぼ確実に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 つのいずれかになります。

  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)、移動元の値に対してのみ 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;
}

上記の実装は自己代入を許容しますが*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;
}

上記の実装では、コピー代入演算子と同じように自己代入をノーオペレーションにすることで許容しています。これでもいいです。

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.

于 2012-02-17T03:40:23.237 に答える
12

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

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

const(非) l値参照を介して戻ることに注意してください。

どちらの種類の直接割り当てでも、自己割り当てをチェックするのではなく、自己割り当てがクラッシュ アンド バーンを引き起こさないようにすることが標準です。通常、誰も明示的に呼び出しx = xたりy = std::move(y)呼び出したりすることはありませんが、特に複数の関数を介したエイリアシングは、自己割り当てにつながるa = bc = std::move(d)、自己割り当てになる可能性があります。自己代入の明示的なチェック、つまりthis == &rhstrue の場合に関数の中身をスキップすることは、自己代入の安全性を確保する 1 つの方法です。しかし、これは最悪の方法の 1 つです。なぜなら、(できれば) まれなケースを最適化する一方で、より一般的なケース (分岐やキャッシュ ミスによる) の最適化に反対するからです。

(少なくとも) オペランドの 1 つが直接一時的なオブジェクトである場合、自己割り当てのシナリオはあり得ません。一部の人々は、そのケースを想定してコードを最適化することを提唱し、その仮定が間違っているとコードが自殺するほど愚かになる. 私は、ユーザーに対して同一オブジェクト チェックをダンプするのは無責任だと言います。コピー代入についてはそのような議論はしません。move-assignment の位置を逆にするのはなぜですか?

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

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 ランク下げることです。(ところで、個々の要素の値は独立していると仮定しています。他の値と比較して一部の値を制限する不変の制約はありません。)

これと同じタイプの move-assignment を見てみましょう。

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が提供されていない場合は、おそらく free-functionswapをスワップ可能な型のフレンドとしてマークする必要があります。move を use にカスタマイズする場合はswap、独自のスワッピング コードを提供する必要があります。標準コードは型の move コードを呼び出します。これにより、move によってカスタマイズされた型に対して無限の相互再帰が発生します。

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

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

この移動割り当ては、swap呼び出しが安全であるため、自己割り当てに対して自動的に安全であることに注意してください。また、強力な例外セーフです。問題は、不要なリソースの保持です。宛先の古いリソースは概念的には不要ですが、ソース オブジェクトを有効に保つためだけに残っています。ソース オブジェクトの予定された破棄がかなり先になる場合は、リソース スペースを浪費していることになります。さらに悪いことに、リソース スペースの合計が制限されていて、(新しい) ソース オブジェクトが正式に終了する前に他のリソースの請願が発生する場合もあります。

この問題は、ムーブ割り当て中のセルフ ターゲティングに関する現在の第一人者のアドバイスが物議を醸している原因です。リソースを残さずに move-assignment を記述する方法は次のようになります。

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)最初の行を食べられるようにすることです (気分が良い場合)。

別の方法は、コピー代入をユニファイド代入なしで強力な例外セーフにする方法を研究し、それをムーブ代入に適用することです。

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

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

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

自己代入が発生すると、othertoもtemp空になりますthis。次に、ターゲットオブジェクトは、いつスワップするかtempでリソースを取り戻します。thisクレームの死はtemp空のオブジェクトを要求します。これは実質的に何もしないはずです。this/otherオブジェクトはそのリソースを保持します。

move-construction と swapping も同様である限り、move-assignment は never-throw であるべきです。自己割り当て中にも安全であることのコストは、低レベルの型よりもいくつかの命令が増えることです。

于 2012-03-13T17:06:38.690 に答える
6

私は、自己代入の安全な演算子が必要であるが、の実装で自己代入チェックを書きたくないというキャンプにいますoperator=。実際、私はまったく実装したくありませんoperator=。デフォルトの動作が「箱から出してすぐに」機能することを望んでいます。最高の特別会員は無料で来る人です。

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

式 戻り値の型 戻り値 事後条件
t = rv T& tt は代入前の rv の値に相当します

ここで、プレースホルダーは次のように記述されtます。および "rvは T 型の右辺値;" です。これらは、標準ライブラリのテンプレートへの引数として使用される型に適用される要件であることに注意してください。ただし、標準の他の場所を見ると、ムーブ代入に関するすべての要件がこれに類似していることに気付きます。

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

于 2012-02-17T04:12:34.657 に答える
2

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

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
}

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

于 2012-02-17T03:19:58.903 に答える