【问题标题】:Should the Copy-and-Swap Idiom become the Copy-and-Move Idiom in C++11?Copy-and-Swap 成语是否应该成为 C++11 中的 Copy-and-Move 成语?
【发布时间】:2014-07-23 17:27:51
【问题描述】:

正如this answer 中所解释的,copy-and-swap 习惯用法如下实现:

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

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

答案提到您可以交换或移动临时变量。它主要讨论交换。但是,如果没有经过编译器优化,交换会涉及三个移动操作,并且在更复杂的情况下会做额外的额外工作。当您想要的只是将临时对象移动到分配对象中。

考虑这个更复杂的例子,涉及observer pattern。在此示例中,我手动编写了赋值运算符代码。重点是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 中将复制和交换习语作为复制和移动习语会更好,还是我错过了一些重要的事情?

【问题讨论】:

  • 需要注意的是,std::swap 的默认实现将使用移动构造函数,如果你不自己专门化它,导致代码几乎等同于你的第二个示例。
  • @ComicSansMS 不过,它将使用一个移动构造和两个移动分配。无论如何,移动代码已经存在,对于移动构造函数,所以似乎不需要使用交换来代替重用移动构造函数代码。
  • @Aberrant:对你的编译器表现出一点信心,它擅长消除无用的东西。
  • 这个问题非常冗长,可能不到这个长度的一半。你能把它清理干净吗?
  • @mydogisbox 好点,我会试试的。

标签: c++ c++11 move-semantics assignment-operator copy-and-swap


【解决方案1】:

给予每个特殊成员应有的温柔关爱,并尽量默认他们:

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::vectorstd::string 不使用复制/交换?性能不佳是原因。如果BigClass 包含任何std::vectors 或std::strings(这似乎很可能),那么最好的办法是从您的特殊成员中调用他们的特殊成员。以上是如何做到的。

如果您需要对作业进行强大的异常安全性,请参阅幻灯片了解如何在性能之外提供此功能(搜索“strong_assign”)。

【讨论】:

  • 重新“查看幻灯片”,通常最好在 SO 答案中包含相关信息,而不是引导用户离开现场。至少在我的 Firefox 中,幻灯片没有提供有关性能的信息。看到这个screenshot。顺便提一句。我只是在猜测幻灯片中的信息可能在哪里,如果有的话。
  • 为什么这里是显式的swap,而不是默认的?
  • 幻灯片没有解释为什么复制和交换更慢,他们只是说它是。另外我不明白为什么幻灯片提到容量,因为交换向量只是交换指针而容量没有变化。
  • @DavidBrown:关键是回收容量足以完成这项工作。在极端情况下(图的右侧),lhs 的容量总是足以处理 rhs 的大小,因此永远不需要释放/分配内存。避免访问堆是优化的关键。
  • @TemplateRex:在这个例子中,它既没有帮助也没有伤害。我在上面没有使用它的原因之一是 make_unique 直到 C++14 才成为标准(实际上还不是官方的,但 C++14 现在是一个相当安全的选择)。我想保留我的答案 C++11。一般来说,一旦你的工具箱中有make_unique,我会鼓励使用它,这样你就不必决定它是否是一种改进。 make_unique 永远不会更糟,有时会更好。
【解决方案2】:

首先,只要你的类是可移动的,通常不需要在 C++11 中编写swap 函数。默认的swap 将诉诸移动:

void swap(T& left, T& right) {
    T tmp(std::move(left));
    left = std::move(right);
    right = std::move(tmp);
}

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

其次,基于此,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;

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

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

【讨论】:

  • 我同意,如果您仍然要移动,交换的成本可能可以忽略不计,但它是否比仅重用移动构造函数主体中的代码更有优势?如我的问题所示,您可以将移动构造函数的主体放在单独的函数中以避免代码重复。在我看来,交换充其量是平等的,否则比移动更糟糕。从本质上讲,我在这个答案中错过的是交换是更好选择的原因。
  • @Aberrant:通过辅助函数实现移动构造函数的主体并不是特别有效,因为构造函数完成的大部分工作都在初始化列表中,而不是主体.
  • @Aberrant:就性能而言,交换确实充其量是相等的,但它也是您不必编写和维护的代码;具体来说,当您添加新的数据成员时,如果您手动编写复制/移动构造函数和交换函数,则需要进行大量维护,而编译器生成的默认版本始终是最新的。至于性能打击......事实上应该没有。移动很便宜(一开始),只要优化器可以将移动构造函数内联到 swap 专业化的主体中,它就会消除效率低下的问题。
  • 在copy assignment中,为什么要通过引用T&amp; T::operator=(T const&amp; left)而不是通过值T&amp; T::operator=(T left)来捕获呢?按值捕获不会消除对tmp 的需要吗?
  • @becko:不仅可以,而且还不需要编写两个赋值运算符。
【解决方案3】:

我已经很久没有问这个问题了,我已经知道答案有一段时间了,但是我推迟了写答案。在这里。

答案是否定的。 Copy-and-swap 成语不应成为 Copy-and-move 成语。

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

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

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

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

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

【讨论】:

    猜你喜欢
    • 2022-12-04
    • 2014-12-21
    • 1970-01-01
    • 2014-03-28
    • 2019-06-14
    • 2011-10-14
    • 2022-12-01
    • 2012-03-13
    • 1970-01-01
    相关资源
    最近更新 更多