Copy-and-Swap Idiom应该成为C++ 11中的复制和移动习惯吗?

Abe*_*ant 23 c++ assignment-operator move-semantics copy-and-swap c++11

本回答所述,复制和交换习惯用法如下实现:

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;
    }
};
Run Code Online (Sandbox Code Playgroud)

通过将MyClass的值作为operator =的参数,可以通过复制构造函数或移动构造函数构造参数.然后,您可以安全地从参数中提取数据.这可以防止代码重复并有助于异常安全.

答案提到您可以在临时中交换或移动变量.它主要讨论交换.但是,交换(如果未由编译器优化)涉及三个移动操作,而在更复杂的情况下,还需要额外的额外工作.当你想要的时候,就是临时文件移动到assign-to对象中.

考虑这个更复杂的例子,涉及观察者模式.在这个例子中,我手动编写了赋值运算符代码.重点是移动构造函数,赋值运算符和交换方法:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

显然,此手动编写的赋值运算符中的代码的重复部分与移动构造函数的相同.您可以在赋值运算符中执行交换,行为也是正确的,但它可能执行更多移动并执行额外的注册(在交换中)和取消注册(在析构函数中).

重新使用移动构造函数的代码不是更有意义吗?

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);
    }
Run Code Online (Sandbox Code Playgroud)

在我看来,这种方法从不逊色于交换方法.我是否正确地认为复制和交换习惯用作C++ 11中的复制和移动习惯用法会更好,或者我是否错过了重要的东西?

Mat*_* M. 13

首先,swap只要您的类是可移动的,通常不必在C++ 11中编写函数.默认swap将采取移动:

void swap(T& left, T& right) {
    T tmp(std::move(left));
    left = std::move(right);
    right = std::move(tmp);
}
Run Code Online (Sandbox Code Playgroud)

就是这样,元素被交换了.

其次,基于此,Copy-And-Swap实际上仍然存在:

T& T::operator=(T const& left) {
    using std::swap;
    T tmp(left);
    swap(*this, tmp);
    return *this;
}

// Let's not forget the move-assignment operator to power down the swap.
T& T::operator=(T&&) = default;
Run Code Online (Sandbox Code Playgroud)

将复制和交换(这是一个移动)或移动和交换(这是一个移动),并应始终达到接近最佳性能.可能有一些冗余分配,但希望您的编译器会处理它.

编辑:这只实现了复制赋值运算符; 还需要一个单独的移动赋值运算符,尽管它可以是默认的,否则会发生堆栈溢出(移动赋值和交换无限期地相互调用).

  • @Aberrant:通过辅助函数实现移动构造函数的主体并不是特别有效,因为**构造函数完成的大部分工作都在初始化列表中,而不是正文**. (4认同)

How*_*ant 13

给每个特殊成员提供应有的温柔关怀,并尽可能地将其默认:

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);
    }
};
Run Code Online (Sandbox Code Playgroud)

如果需要,析构函数可以在上面隐式默认.对于此示例,其他所有内容都需要明确定义或默认.

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

复制/交换习惯用语可能会让您失去性能(请参阅幻灯片).例如,曾经想知道为什么高性能/经常使用std ::类似std::vectorstd::string不使用copy/swap?表现不佳是原因.如果BigClass包含任何std::vectors或std::strings(似乎很可能),您最好的选择是从您的特殊成员中调用他们的特殊成员.以上是如何做到这一点.

如果您在作业中需要强大的异常安全性,请参阅幻灯片,了解除性能之外的其他方法(搜索"strong_assign").


Abe*_*ant 5

自从我问这个问题已经很长时间了,现在我已经知道了一段时间的答案,但是我推迟了为它写答案。这里是。

答案是不。Copy-and-swap 习语不应该变成 Copy-and-move 习语。

Copy-and-swap(也是Move-construct-and-swap)的一个重要部分是通过安全清理实现赋值运算符的方法。旧数据被交换到复制构造或移动构造的临时数据中。操作完成后,临时文件被删除,并调用其析构函数。

交换行为是为了能够重用析构函数,因此您不必在赋值运算符中编写任何清理代码。

如果没有要完成的清理行为而只有赋值,那么您应该能够将赋值运算符声明为默认值,并且不需要复制和交换。

移动构造函数本身通常不需要任何清理行为,因为它是一个新对象。一般的简单方法是让移动构造函数调用默认构造函数,然后将所有成员与移动自对象交换。移出的对象将像一个平淡的默认构造对象。

但是,在这个问题的观察者模式示例中,这实际上是一个例外,您必须进行额外的清理工作,因为需要更改对旧对象的引用。一般来说,我建议您尽可能让观察者和可观察者以及其他基于引用的设计结构不可移动。