简体   繁体   English

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

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

I saw many code implementing rule of five in terms of copy and swap, but I think we can use a move function to replace the swap function as in the following code: 我看到许多代码在复制和交换方面实现了五个规则,但我认为我们可以使用移动函数来替换交换函数,如下面的代码所示:

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

This code, I think 我想这个代码

  1. Exception safe 例外安全
  2. Require less typing, as many function just call move_to_this(), and copy assignment and move assignment are unified in one single function 需要更少的输入,因为许多函数只调用move_to_this(),并且复制赋值和移动赋值在一个函数中统一
  3. More efficient than copy-and-swap, as swap involves 3 assignments, while here just 2, and this code doesn't suffer the problems mentioned in This Link 比复制和交换更有效,因为交换涉及3个分配,而这里只有2个,并且此代码不会遇到此链接中提到的问题

Am I right? 我对吗?

Thanks 谢谢

Edit: 编辑:

  1. As @Leon pointed out, maybe a dedicated function for freeing resource is needed, to avoid code duplication in move_to_this() and destructor 正如@Leon指出的那样,可能需要一个用于释放资源的专用函数,以避免在move_to_this()和析构函数中重复代码
  2. As @thorsan pointed out, for extreme performance concern, it's better to seperate DumbArray& operator=(DumbArray that) { move_to_this(that); return *this; } 正如@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; } into DumbArray& operator=(const DumbArray &that) { DumbArray temp(that); move_to_this(temp); 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; } (thanks to @MikeMB) and DumbArray& operator=(DumbArray &&that) { move_to_this(that); 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; } to avoid an extra move operatoin DumbArray& operator=(DumbArray &&that) { move_to_this(that); return *this; } ,以避免额外的举动operatoin

    After adding some debug print, I found that no extra move is involved in DumbArray& operator=(DumbArray that) {} when you call it as a move assignment 添加一些调试打印后,我发现当你将其称为移动赋值时, DumbArray& operator=(DumbArray that) {}中不会涉及额外的移动

  3. As @Erik Alapää pointed out, a self-assignment check is needed before delete in move_to_this() 正如@ErikAlapää指出的那样,在move_to_this() delete之前需要进行自我分配检查

comments inline, but briefly: 评论内联,但简要说明:

  • you want all move assignments and move constructors to be noexcept if at all possible. 如果可能的话,你想要所有的移动赋值和移动构造函数都是noexcept The standard library is much faster if you enable this, because it can elide any exception handling from algorithms which reorder a sequence of your object. 如果启用此功能,因为它的Elid可以任何异常从中重新排列对象的序列算法处理标准库的速度快得多。

  • if you're going to define a custom destructor, make it noexcept. 如果您要定义自定义析构函数,请将其设置为noexcept。 Why open pandora's box? 为什么打开潘多拉的盒子? I was wrong about this. 我错了。 It's noexcept by default. 默认情况下,这是noexcept。

  • In this case, providing the strong exception guarantee is painless and costs almost nothing, so let's do that. 在这种情况下,提供强大的异常保证是无痛的,几乎没有任何成本,所以让我们这样做。

code: 码:

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

There is one further performance improvement one could make in the move-assignment operator. 在移动赋值运算符中可以进一步提高性能。

The solution I have offered provides the guarantee that a moved-from array will be empty (with resources deallocated). 我提供的解决方案提供了一个保证,即移动的数组将为空(资源被释放)。 This may not be what you want. 这可能不是你想要的。 For example if you tracked the capacity and the size of a DumbArray separately (for example, like std::vector), then you may well want any allocated memory in this to be retained in that after the move. 例如,如果你跟踪的容量和DumbArray的单独的大小(例如,比如std ::矢量),则很可能需要的任何已分配的内存在this被保持在that移动后。 This would then allow that to be assigned to while possibly getting away without another memory allocation. 然后that这将允许将其分配给可能在没有另外的存储器分配的情况下离开。

To enable this optimisation, we simply implement the move-assign operator in terms of (noexcept) swap: 为了实现这种优化,我们只需根据(noexcept)swap实现move-assign运算符:

so from this: 所以从这个:

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

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

In the case of the DumbArray, it's probably worth using the more relaxed form in practice, but beware of subtle bugs. 在DumbArray的情况下,在实践中使用更放松的形式可能是值得的,但要注意微妙的错误。

eg 例如

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

The only (small) problem with your code is the duplication of functionality between move_to_this() and the destructor, which is a maintenance issue should your class need to be changed. 您的代码唯一(小)问题是move_to_this()和析构函数之间的功能重复,如果您的类需要更改,这是一个维护问题。 Of course it can be solved by extracting that part into a common function destroy() . 当然,可以通过将该部分提取到公共函数destroy()来解决。

My critique of the "problems" discussed by Scott Meyers in his blog post: 我对Scott Meyers在他博客文章中讨论的“问题”的批评:

He tries to manually optimize where the compiler could do an equally good job if it is smart enough. 他试图手动优化编译器可以做得同样好的工作,如果它足够聪明的话。 The rule-of-five can be reduced to the rule-of-four by 五法则可以​​减少到四法则

  • providing only the copy assignment operator that takes its argument by value and 仅提供通过值和参数获取其参数的复制赋值运算符
  • not bothering to write the move assignment operator (exactly what you did). 没有费心去编写移动赋值运算符(正是你所做的)。

This automatically solves the problem of the resources of the left-hand-side object being swapped into the right-hand-side object and not being immediately released if the right-hand-side object is not a temporary. 这自动解决了左侧对象的资源被交换到右侧对象的问题,并且如果右侧对象不是临时对象则不会立即释放。

Then, inside the implementation of the copy assignment operator according to the copy-and-swap idiom, swap() will take as one of its arguments an expiring object. 然后,在复制赋值运算符的实现内部,根据复制和交换习惯用法, swap()将其作为其参数之一作为到期对象。 If the compiler can inline the destructor of the latter, then it will definitely eliminate the extra pointer assignment - indeed, why save the pointer that is going to be delete ed on the next step? 如果编译器可以内联后者的析构函数,那么它肯定会消除额外的指针赋值 - 实际上,为什么要保存将在下一步delete的指针?

My conclusion is that it is simpler to follow the well established idiom instead of slightly complicating the implementation for the sake of micro-optimizations that are well within the reach of a mature compiler. 我的结论是,遵循成熟的惯用语而不是为了微观优化而使实现略微复杂化更为简单,这些微优化在成熟编译器的范围内。

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

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