什么是四(一半)?

jpf*_*342 15 c++ copy-constructor assignment-operator copy-and-swap c++11

为了正确处理对象复制,经验法则是三规则.使用C++ 11,移动语义是一个东西,所以它是五条规则.然而,在这里和互联网上的讨论中,我也看到了对四条规则(一半)的引用,它是五条规则和复制交换习语的组合.

究竟是什么(四分之一)呢?需要实现哪些功能,每个功能的主体应该是什么样的?一半的功能是什么?与五法则相比,这种方法有任何缺点或警告吗?

这是一个类似于我当前代码的参考实现.如果这不正确,那么正确的实现会是什么样的?

//I understand that in this example, I could just use `std::unique_ptr`.
//Just assume it's a more complex resource.
#include <utility>

class Foo {
public:
    //We must have a default constructor so we can swap during copy construction.
    //It need not be useful, but it should be swappable and deconstructable.
    //It can be private, if it's not truly a valid state for the object.
    Foo() : resource(nullptr) {}

    //Normal constructor, acquire resource
    Foo(int value) : resource(new int(value)) {}

    //Copy constructor
    Foo(Foo const& other) {
        //Copy the resource here.
        resource = new int(*other.resource);
    }

    //Move constructor
    //Delegates to default constructor to put us in safe state.
    Foo(Foo&& other) : Foo() {
        swap(other);
    }

    //Assignment
    Foo& operator=(Foo other) {
        swap(other);
        return *this;
    }

    //Destructor
    ~Foo() {
        //Free the resource here.
        //We must handle the default state that can appear from the copy ctor.
        //(The if is not technically needed here. `delete nullptr` is safe.)
        if (resource != nullptr) delete resource;
    }

    //Swap
    void swap(Foo& other) {
        using std::swap;

        //Swap the resource between instances here.
        swap(resource, other.resource);
    }

    //Swap for ADL
    friend void swap(Foo& left, Foo& right) {
        left.swap(right);
    }

private:
    int* resource;
};
Run Code Online (Sandbox Code Playgroud)

Jar*_*d42 13

究竟是什么(四分之一)呢?

"四大(一半)规则"指出,如果你实施其中一个

  • 复制构造函数
  • 赋值运算符
  • 移动构造函数
  • 析构函数
  • 交换功能

那么你必须有一个关于其他人的政策.

需要实现哪些功能,每个功能的主体应该是什么样的?

  • 默认构造函数(可以是私有的)
  • 复制构造函数(这里有真正的代码来处理你的资源)
  • 移动构造函数(使用默认构造函数和交换):

    S(S&& s) : S{} { swap(*this, s); }
    
    Run Code Online (Sandbox Code Playgroud)
  • 赋值运算符(使用构造函数和交换)

    S& operator=(S s) { swap(*this, s); }
    
    Run Code Online (Sandbox Code Playgroud)
  • 析构函数(资源的深层副本)

  • friend swap(没有默认实现:/你应该想要交换每个成员).这一点与swap成员方法相反:std::swap使用move(或copy)构造函数,这将导致无限递归.

一半的功能是什么?

从上一篇文章:

"为了实现Copy-Swap习惯用法,你的资源管理类还必须实现一个swap()函数来执行逐个成员的交换(这是你的"......(和一半)")

所以swap方法.

与五法则相比,这种方法有任何缺点或警告吗?

我已经写过的警告是要写正确的交换以避免无限递归.


Nir*_*man 6

与五法则相比,这种方法有什么缺点或警告吗?

虽然它可以节省代码重复,但使用复制和交换只会导致更糟糕的类,直言不讳。你正在损害你班级的表现,包括移动赋值(如果你使用统一赋值运算符,我也不喜欢它),这应该非常快。作为交换,您将获得强大的异常保证,这乍一看似乎不错。问题是,您可以使用简单的泛型函数从任何类中获得强大的异常保证:

template <class T>
void copy_and_swap(T& target, T source) {
    using std::swap;
    swap(target, std::move(source));
}
Run Code Online (Sandbox Code Playgroud)

就是这样。所以需要强异常安全的人无论如何都可以得到它。坦率地说,无论如何,强大的异常安全性是一个小众市场。

避免代码重复的真正方法是通过零规则:选择成员变量,这样您就不需要编写任何特殊函数。在现实生活中,我会说 90+% 的时间我看到特殊的成员函数,它们很容易被避免。即使您的类确实具有特殊成员函数所需的某种特殊逻辑,您通常最好将其推成为会员。您的记录器类可能需要在其析构函数中刷新缓冲区,但这不是编写析构函数的理由:编写一个处理刷新的小缓冲区类并将其作为记录器的成员。记录器可能拥有各种可以自动处理的其他资源,您希望让编译器自动生成复制/移动/销毁代码。

关于 C++ 的事情是,特殊函数的自动生成对于每个函数来说要么全有要么全无。这就是拷贝构造函数(如)要么被自动生成的,同时考虑到所有成员,或者你必须写(更糟的是,保持),它的所有的手。所以它强烈地促使你采用一种向下推动事物的方法。

如果您正在编写一个类来管理资源并需要处理这个问题,它通常应该是:a) 相对较小,以及 b) 相对通用/可重用。前者意味着一点重复的代码没什么大不了的,后者意味着你可能不想把性能放在桌面上。

总之,我强烈反对使用复制和交换,以及使用统一赋值运算符。尝试遵循零规则,如果不能,遵循五规则。swap仅当您可以使其比通用交换(执行 3 次移动)更快时才写入,但通常您不必费心。

  • @DanielH 延迟很长,但是...... 4.5 的规则更慢,因为移动分配是比交换更小、更有限的操作。3 次移动的交换基本上是最优的,模排序、特定机器指令等,非常低级的细节。就交换而言,移动分配显然不是最优的。即使对于像 unique_ptr 这样的东西,它只是底层的原始指针。移动分配只是一个分配(读取旧的+写入新的),和另一个写入(空旧的)。交换是 3 次分配(读取新值 + 写入临时值、读取旧值 + 写入新值、读取临时值 + 写入旧值)。 (2认同)
  • 所以它是 2 次写入 + 1 次读取,而不是 3 次写入 + 3 次读取。当然,真实的情况比这更复杂(例如,最后一次读取没有实际成本,因为它已经在寄存器中),并且编译器*可能*可以帮助您并优化事情。但由于各种原因很难依赖它,这需要更多时间来解释。基本上,归根结底,4.5 的规则只是要求完成更多工作,其中一些工作是不必要的(但它为您提供了强有力的例外保证)。 (2认同)