【问题标题】:A better way to implement copy-and-swap idiom in C++11在 C++11 中实现复制和交换习语的更好方法
【发布时间】:2016-10-21 06:29:17
【问题描述】:

我看到很多代码在复制和交换方面实现了五规则,但我认为我们可以使用移动函数来替换交换函数,如下代码所示:

#include <algorithm>
#include <cstddef>

class DumbArray {
public:
    DumbArray(std::size_t size = 0)
        : size_(size), array_(size_ ? new int[size_]() : nullptr) {
    }

    DumbArray(const DumbArray& that)
        : size_(that.size_), array_(size_ ? new int[size_] : nullptr) {
        std::copy(that.array_, that.array_ + size_, array_);
    }

    DumbArray(DumbArray&& that) : DumbArray() {
        move_to_this(that);
    }

    ~DumbArray() {
        delete [] array_;
    }

    DumbArray& operator=(DumbArray that) {
        move_to_this(that);
        return *this;
    }

private:
    void move_to_this(DumbArray &that) {
        delete [] array_;
        array_ = that.array_;
        size_ = that.size_;
        that.array_ = nullptr;
        that.size_ = 0;
   }

private:
    std::size_t size_;
    int* array_;
};

我认为是这段代码

  1. 异常安全
  2. 需要更少的输入,因为许多函数只需调用 move_to_this(),并且复制赋值和移动赋值统一在一个函数中
  3. 比复制和交换更有效,因为交换涉及 3 个分配,而这里只有 2 个,并且此代码不会遇到This Link 中提到的问题

我说的对吗?

谢谢

编辑:

  1. 正如@Leon 指出的那样,可能需要一个用于释放资源的专用函数,以避免move_to_this() 和析构函数中的代码重复
  2. 正如@thorsan 所指出的,出于极端的性能考虑,最好将DumbArray&amp; operator=(DumbArray that) { move_to_this(that); return *this; } 分隔为DumbArray&amp; operator=(const DumbArray &amp;that) { DumbArray temp(that); move_to_this(temp); return *this; }(感谢@MikeMB)和DumbArray&amp; operator=(DumbArray &amp;&amp;that) { move_to_this(that); return *this; },以避免额外的移动操作

    添加一些调试打印后,我发现DumbArray&amp; operator=(DumbArray that) {} 中没有额外的移动,当你调用它作为移动赋值时

  3. 正如@Erik Alapää 所指出的,在move_to_this() 中的delete 之前需要进行自分配检查

【问题讨论】:

  • 比较适合Code Review?
  • 代码不是异常安全的,因为至少 new 和 delete 可以抛出。
  • @Resurrection,我所说的“异常安全”并不是说它不抛出异常,而是即使抛出异常,它也不会破坏任何东西并使现有对象无效
  • 题外话可能你还没有实现这五个,你实现的赋值运算符既不是复制也不是移动,复制:DumbArray&amp; operator=(DumbArray&amp; that)和移动DumbArray&amp; operator=(DumbArray&amp;&amp; that)。而且copy和move不能一样,所以move_to_this不能用于copy和move。
  • 有没有计划将元素类型从int更改为一些非平凡的类类型?

标签: c++ c++11


【解决方案1】:

cmets 内联,但简短:

  • 如果可能的话,您希望所有移动分配和移动构造函数都是noexcept。如果启用此功能,标准库会快得多,因为它可以从重新排序对象序列的算法中排除任何异常处理。

  • 如果您要定义自定义析构函数,请将其设为 noexcept。为什么要打开潘多拉魔盒?我错了。默认是noexcept。

  • 在这种情况下,提供强大的异常保证是轻而易举的,而且几乎没有成本,所以让我们这样做吧。

代码:

#include <algorithm>
#include <cstddef>

class DumbArray {
public:
    DumbArray(std::size_t size = 0)
    : size_(size), array_(size_ ? new int[size_]() : nullptr) {
    }

    DumbArray(const DumbArray& that)
    : size_(that.size_), array_(size_ ? new int[size_] : nullptr) {
        std::copy(that.array_, that.array_ + size_, array_);
    }

    // the move constructor becomes the heart of all move operations.
    // note that it is noexcept - this means our object will behave well
    // when contained by a std:: container
    DumbArray(DumbArray&& that) noexcept
    : size_(that.size_)
    , array_(that.array_)
    {
        that.size_ = 0;
        that.array_ = nullptr;
    }

    // noexcept, otherwise all kinds of nasty things can happen
    ~DumbArray() // noexcept - this is implied.
    {
        delete [] array_;
    }

    // I see that you were doing by re-using the assignment operator
    // for copy-assignment and move-assignment but unfortunately
    // that was preventing us from making the move-assignment operator
    // noexcept (see later)
    DumbArray& operator=(const DumbArray& that)
    {
        // copy-swap idiom provides strong exception guarantee for no cost
        DumbArray(that).swap(*this);
        return *this;
    }

    // move-assignment is now noexcept (because move-constructor is noexcept
    // and swap is noexcept) This makes vector manipulations of DumbArray
    // many orders of magnitude faster than they would otherwise be
    // (e.g. insert, partition, sort, etc)
    DumbArray& operator=(DumbArray&& that) noexcept {
        DumbArray(std::move(that)).swap(*this);
        return *this;
    }


    // provide a noexcept swap. It's the heart of all move and copy ops
    // and again, providing it helps std containers and algorithms 
    // to be efficient. Standard idioms exist because they work.
    void swap(DumbArray& that) noexcept {
        std::swap(size_, that.size_);
        std::swap(array_, that.array_);
    }

private:
    std::size_t size_;
    int* array_;
};

在移动赋值运算符中还可以进一步提高性能。

我提供的解决方案保证了移出的数组将为空(资源被释放)。这可能不是你想要的。例如,如果您分别跟踪 DumbArray 的容量和大小(例如,像 std::vector),那么您可能希望在移动后将this 中分配的任何内存保留在that 中。这将允许 that 被分配到,同时可能在没有其他内存分配的情况下离开。

为了实现这种优化,我们只需根据 (noexcept) 交换实现 move-assign 运算符:

因此:

    /// @pre that must be in a valid state
    /// @post that is guaranteed to be empty() and not allocated()
    ///
    DumbArray& operator=(DumbArray&& that) noexcept {
        DumbArray(std::move(that)).swap(*this);
        return *this;
    }

到这里:

    /// @pre that must be in a valid state
    /// @post that will be in an undefined but valid state
    DumbArray& operator=(DumbArray&& that) noexcept {
        swap(that);
        return *this;
    }

对于 DumbArray,在实践中可能值得使用更宽松的形式,但要注意细微的错误。

例如

DumbArray x = { .... };
do_something(std::move(x));

// here: we will get a segfault if we implement the fully destructive
// variant. The optimised variant *may* not crash, it may just do
// something_else with some previously-used data.
// depending on your application, this may be a security risk 

something_else(x);   

【讨论】:

  • 现在默认的析构函数不是noexcept (true)吗?
  • @JDługosz 仅在默认 IIRC 的情况下
  • @RichardHodges C++11 Working Draft N3376 似乎不同意你的观点(12.4 析构函数,第 3 项):A declaration of a destructor that does not have an exception-specification is implicitly considered to have the same exception-specification as an implicit declaration
【解决方案2】:

您的代码的唯一(小)问题是move_to_this() 和析构函数之间的功能重复,如果您的类需要更改,这是一个维护问题。当然可以通过将那部分提取到一个通用函数destroy()中来解决。

我对 Scott Meyers 在他的博文中讨论的“问题”的批评:

如果编译器足够聪明,他会尝试手动优化编译器可以做得同样出色的地方。五法则可以​​通过以下方式简化为四法则

  • 仅提供按值获取参数的复制赋值运算符,并且
  • 不必费心编写移动赋值运算符(正是您所做的)。

这会自动解决左侧对象的资源被交换到右侧对象中,如果右侧对象不是临时对象,则不会立即释放的问题。

然后,在根据复制和交换习惯用法的复制赋值运算符的实现中,swap() 将把一个过期对象作为其参数之一。如果编译器可以内联后者的析构函数,那么它肯定会消除额外的指针赋值——确实,为什么要保存下一步将要被deleteed 的指针?

我的结论是,遵循成熟的习惯用法会更简单,而不是为了实现成熟编译器能够实现的微优化而稍微复杂化。

【讨论】:

  • 对于您的第一点,我不这么认为,您可能会错过移动构造函数的: DumbArray() 部分
  • 我认为可以通过创建另一个专用于释放资源的函数来解决重复问题,该函数由move_to_this() 和析构函数调用
  • 我不太相信 - 在实践中 - 复制和移动比复制和交换更复杂,但我的一般立场是也抛出复制和交换成语作为 0,1,2,3,4,5... 的规则无论如何都在窗外 - 至少作为默认指南。这是 c++03 中的(相对)简单明了的指导方针,但在现代 c++11 及以后的代码中,无论如何你都无法真正避免对每个成员函数做出每个类的决定(大多数情况下) ,默认值会这样做)。但这个讨论可能是这里的主题。
猜你喜欢
  • 2012-10-06
  • 1970-01-01
  • 2011-10-28
  • 2017-02-23
  • 2013-08-10
  • 1970-01-01
  • 2023-03-23
  • 1970-01-01
  • 2011-11-22
相关资源
最近更新 更多