25

この回答で説明されているように、コピー アンド スワップ イディオムは次のように実装されます。

class MyClass
{
private:
    BigClass data;
    UnmovableClass *dataPtr;

public:
    MyClass()
      : data(), dataPtr(new UnmovableClass) { }
    MyClass(const MyClass& other)
      : data(other.data), dataPtr(new UnmovableClass(*other.dataPtr)) { }
    MyClass(MyClass&& other)
      : data(std::move(other.data)), dataPtr(other.dataPtr)
    { other.dataPtr= nullptr; }

    ~MyClass() { delete dataPtr; }

    friend void swap(MyClass& first, MyClass& second)
    {
        using std::swap;
        swap(first.data, other.data);
        swap(first.dataPtr, other.dataPtr);
    }

    MyClass& operator=(MyClass other)
    {
        swap(*this, other);
        return *this;
    }
};

operator= のパラメーターとして MyClass の値を持つことにより、コピー コンストラクターまたはムーブ コンストラクターのいずれかによってパラメーターを構築できます。その後、パラメーターからデータを安全に抽出できます。これにより、コードの重複が防止され、例外の安全性が向上します。

答えは、変数を一時的に交換または移動できることを示しています。主にスワッピングについて説明します。ただし、コンパイラによって最適化されていない場合、スワップには 3 つの移動操作が含まれ、より複雑なケースでは追加の作業が必要になります。必要な場合は、一時オブジェクトを割り当て先オブジェクトに移動するだけです。

オブザーバー パターンを含む、このより複雑な例を考えてみましょう。この例では、代入演算子のコードを手動で記述しました。move コンストラクター、代入演算子、swap メソッドに重点が置かれています。

class MyClass : Observable::IObserver
{
private:
    std::shared_ptr<Observable> observable;

public:
    MyClass(std::shared_ptr<Observable> observable) : observable(observable){ observable->registerObserver(*this); }
    MyClass(const MyClass& other) : observable(other.observable) { observable.registerObserver(*this); }
    ~MyClass() { if(observable != nullptr) { observable->unregisterObserver(*this); }}

    MyClass(MyClass&& other) : observable(std::move(other.observable))
    {
        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }

    friend void swap(MyClass& first, MyClass& second)
    {
        //Checks for nullptr and same observable omitted
            using std::swap;
            swap(first.observable, second.observable);

            second.observable->unregisterObserver(first);
            first.observable->registerObserver(first);
            first.observable->unregisterObserver(second);
            second.observable->registerObserver(second);
    }

    MyClass& operator=(MyClass other)
    {
        observable->unregisterObserver(*this);
        observable = std::move(other.observable);

        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }
}

明らかに、この手動で記述された代入演算子のコードの重複部分は、ムーブ コンストラクターの部分と同じです。代入演算子でスワップを実行すると動作は正しくなりますが、より多くの移動が実行され、余分な登録 (スワップ内) と登録解除 (デストラクタ内) が実行される可能性があります。

代わりに、ムーブ コンストラクターのコードを再利用する方がはるかに理にかなっているのではないでしょうか?

private:
    void performMoveActions(MyClass&& other)
    {
        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }

public:
    MyClass(MyClass&& other) : observable(std::move(other.observable))
    {
        performMoveActions(other);
    }

    MyClass& operator=(MyClass other)
    {
        observable->unregisterObserver(*this);
        observable = std::move(other.observable);

        performMoveActions(other);
    }

このアプローチは、スワップ アプローチよりも決して劣っていないように見えます。C++ 11 では、コピー アンド スワップ イディオムの方がコピー アンド ムーブ イディオムとして適していると考えるのは正しいですか、それとも何か重要なことを見逃していましたか?

4

3 に答える 3

15

それぞれの特別なメンバーに、それに値する優しい愛のケアを与え、可能な限りデフォルトにするようにしてください。

class MyClass
{
private:
    BigClass data;
    std::unique_ptr<UnmovableClass> dataPtr;

public:
    MyClass() = default;
    ~MyClass() = default;
    MyClass(const MyClass& other)
        : data(other.data)
        , dataPtr(other.dataPtr ? new UnmovableClass(*other.dataPtr)
                                : nullptr)
        { }
    MyClass& operator=(const MyClass& other)
    {
        if (this != &other)
        {
            data = other.data;
            dataPtr.reset(other.dataPtr ? new UnmovableClass(*other.dataPtr)
                                        : nullptr);
        }
        return *this;
    }
    MyClass(MyClass&&) = default;
    MyClass& operator=(MyClass&&) = default;

    friend void swap(MyClass& first, MyClass& second)
    {
        using std::swap;
        swap(first.data, second.data);
        swap(first.dataPtr, second.dataPtr);
    }
};

必要に応じて、上記のデストラクタを暗黙的にデフォルトにすることができます。この例では、他のすべてを明示的に定義するか、デフォルトにする必要があります。

参照: http://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf

コピー/スワップのイディオムは、パフォーマンスを低下させる可能性があります (スライドを参照)。たとえば、高パフォーマンス/頻繁に使用される std::types がコピー/スワップを使用しない理由を疑問に思ったことはstd::vectorありませんか? std::string原因はパフォーマンスの低下です。s またはsBigClassが含まれている場合(可能性が高いと思われます)、最善の策は、特別なメンバーから特別なメンバーを呼び出すことです。やり方は以上です。std::vectorstd::string

割り当てに強力な例外安全性が必要な場合は、パフォーマンスに加えてそれを提供する方法についてスライドを参照してください (「strong_assign」を検索してください)。

于 2014-06-03T14:39:22.637 に答える