繁体   English   中英

在C ++ 11中实现复制和交换习惯用法的更好方法

[英]A better way to implement copy-and-swap idiom in C++11

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

#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个,并且此代码不会遇到此链接中提到的问题

我对吗?

谢谢

编辑:

  1. 正如@Leon指出的那样,可能需要一个用于释放资源的专用函数,以避免在move_to_this()和析构函数中重复代码
  2. 正如@thorsan指出的那样,对于极端的性能问题,最好分开 DumbArray& operator=(DumbArray that) { move_to_this(that); return *this; } DumbArray& operator=(DumbArray that) { move_to_this(that); return *this; } DumbArray& operator=(DumbArray that) { move_to_this(that); return *this; }DumbArray& operator=(const DumbArray &that) { DumbArray temp(that); move_to_this(temp); return *this; } DumbArray& operator=(const DumbArray &that) { DumbArray temp(that); move_to_this(temp); return *this; } DumbArray& operator=(const DumbArray &that) { DumbArray temp(that); move_to_this(temp); return *this; } (多亏@MikeMB)和 DumbArray& operator=(DumbArray &&that) { move_to_this(that); return *this; } DumbArray& operator=(DumbArray &&that) { move_to_this(that); return *this; } DumbArray& operator=(DumbArray &&that) { move_to_this(that); return *this; } ,以避免额外的举动operatoin

    添加一些调试打印后,我发现当你将其称为移动赋值时, DumbArray& operator=(DumbArray that) {}中不会涉及额外的移动

  3. 正如@ErikAlapää指出的那样,在move_to_this() delete之前需要进行自我分配检查

评论内联,但简要说明:

  • 如果可能的话,你想要所有的移动赋值和移动构造函数都是noexcept 如果启用此功能,因为它的Elid可以任何异常从中重新排列对象的序列算法处理标准库的速度快得多。

  • 如果您要定义自定义析构函数,请将其设置为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 ::矢量),则很可能需要的任何已分配的内存在this被保持在that移动后。 然后that这将允许将其分配给可能在没有另外的存储器分配的情况下离开。

为了实现这种优化,我们只需根据(noexcept)swap实现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);   

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

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

他试图手动优化编译器可以做得同样好的工作,如果它足够聪明的话。 五法则可以​​减少到四法则

  • 仅提供通过值和参数获取其参数的复制赋值运算符
  • 没有费心去编写移动赋值运算符(正是你所做的)。

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

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

我的结论是,遵循成熟的惯用语而不是为了微观优化而使实现略微复杂化更为简单,这些微优化在成熟编译器的范围内。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM