在C++11中,编写Copy/Move/operator=三合一的“正确”方式是什么?

10
在这个阶段,编写复制构造函数和赋值运算符是定义良好的;快速搜索会导向许多如何正确编写这些函数的结果。现在移动构造函数已经加入了其中,是否有一种新的“最佳”方法呢?

1
有一个不错的SO问题,你可能想看一下。 - Jesse Good
1
这个问题太宽泛了。你需要将其缩小到一个具体的场景。因为没有一种“明确定义”的方法来编写每个类的复制构造函数和赋值运算符的配方。对于你的问题也是如此。 - Johannes Schaub - litb
为什么这个问题太宽泛了?对于复制构造函数和赋值运算符,有一个普遍接受的模式,为什么移动构造函数没有呢? - moswald
如果你说的“拷贝构造函数和赋值运算符的普遍接受模式”是指“拷贝并交换”,请参考此链接:https://dev59.com/6mox5IYBdhLWcg3wQSKp#9322542。 - Howard Hinnant
4个回答

13

最好的情况是,它们只需是= default;,因为成员类型应该是资源管理类型,可以从您隐藏移动细节,比如std::unique_ptr。只有那些“低级”类型的实现者才需要处理这个问题。

请记住,仅当您持有外部(对于您的对象而言)资源时才需要关注移动语义。对于“平面”类型来说,它是完全无用的。


1
那是一个正确的观点。我想我本可以加上“如果你要编写自己的程序”的限定词,但你的答案对于那些没有意识到不需要这样做的人来说非常正确。然而,在我的情况下,我持有一个外部资源,这也是我提出这个问题的原因。 :) - moswald
1
小的重构可能会消除这个问题,不过我会考虑一下。我不知道最终结果是否会更简单。 - moswald
1
推荐使用unique_ptr而不是shared_ptr。尽管两者都可以清理任意资源。 - Mooing Duck
1
实际上,只有当所有子对象的组合移动状态符合包含类型的不变量时,“= default”才适用于您,这与子对象是否是“资源管理类型”没有固有关系。通常情况下,当包含类型可以默认构造时,这种情况才会发生,但即使在这种情况下也可能不是如此。很容易想出反例。 - Dave Abrahams
1
@Xeo:是的,可以构建一个逻辑上连贯的世界观,在这个世界观中,类具有“不变量”和在这些“不变量”之外的“特殊状态”,但在那个世界里思考正确性比在真正的不变量(除了突变,等等)的世界中要困难得多。我还没有遇到过需要更复杂方法的实际案例。 - Dave Abrahams
显示剩余7条评论

6

最好的方法是让编译器生成它们。这也是C++03中最好的方法,如果您成功实现了这一点,当您迁移到C++11时,您的C++03类将自动成为“可移动启用”。

大多数资源管理问题可以通过编写单一资源管理类的非复制构造函数和析构函数来解决,然后只使用这些类来创建组合类,再加上智能指针(例如std::unique_ptr)和容器类来构建更丰富的对象。


4
使用clang/libc++:
#include <chrono>
#include <iostream>
#include <vector>

#if SLOW_DOWN

class MyClass
{
    void Swap(MyClass &other)
    {
        std::swap(other.member, member);
    }

public:
    MyClass()
        : member()
    {
    }

    MyClass(const MyClass &other)
        : member(other.member)
    {
    }

    MyClass(MyClass &&other)
        : member(std::move(other.member))
    {
    }

    MyClass &operator=(MyClass other)
    {
        other.Swap(*this);
        return *this;
    }

private:
    int member;
};

#else

class MyClass
{
public:
    MyClass()
        : member()
    {
    }

private:
    int member;
};

#endif

int main()
{
    typedef std::chrono::high_resolution_clock Clock;
    typedef std::chrono::duration<float, std::milli> ms;
    auto t0 = Clock::now();
    for (int k = 0; k < 100; ++k)
    {
        std::vector<MyClass> v;
        for (int i = 0; i < 1000000; ++i)
            v.push_back(MyClass());
    }
    auto t1 = Clock::now();
    std::cout << ms(t1-t0).count() << " ms\n";
}

$ clang++ -stdlib=libc++ -std=c++11 -O3 -DSLOW_DOWN test.cpp 
$ a.out
519.736 ms
$ a.out
517.036 ms
$ a.out
524.443 ms

$ clang++ -stdlib=libc++ -std=c++11 -O3  test.cpp 
$ a.out
463.968 ms
$ a.out
458.702 ms
$ a.out
464.441 ms

这个测试表明,速度差大约为12%。
解释:其中一个定义有一个简单的复制构造函数和复制赋值运算符,而另一个则没有。在C++11中,“简单”具有实际意义。这意味着实现允许使用memcpy来复制您的类,甚至可以复制大型数组。因此,最好让您的特殊成员变得简单(trivial),如果可以的话。这意味着让编译器定义它们。尽管您仍然可以使用= default声明它们,如果您愿意的话。

Howard,“SLOW_DOWN”只是非常低效的版本。这甚至与编译器生成的构造函数等无关... 这里有两个在ideone.com上的代码:第一个使用了OP的版本 T & operator( T )第二个使用了明确定义的T &( T && )T & operator=( T && )。比较 -----------之间的输出... - lapk
1
@AzzA:我只是在比较原帖作者的解决方案和我认为更好的解决方案。没有多余的话。 - Howard Hinnant

2
这是我想到的,但我不知道是否存在更加优化的解决方案。
class MyClass
{
    void Swap(MyClass &other)
    {
        std::swap(other.member, member);
    }

public:
    MyClass()
        : member()
    {
    }

    MyClass(const MyClass &other)
        : member(other.member)
    {
    }

    MyClass(MyClass &&other)
        : member(std::move(other.member))
    {
    }

    MyClass &operator=(MyClass other)
    {
        other.Swap(*this);
        return *this;
    }

private:
    int member;
};

你为什么要将交换(swap)函数设为私有(private)? - ronag
@ronag:在我的实际课程中,目前没有真正需要它的地方。我宁愿不公开API,因为后来可能会有人使用,可能会使用不正确(尽管我不知道他们如何滥用像交换这样简单的东西)。 - moswald
4
如果MyClass确实应该具有这些语义,那么这可能是编写特殊成员的最糟糕(性能最差)的方式,尽管仍然被认为是正确的。抱歉这么直接,但我认为你应该知道。 - Howard Hinnant
@mos 你是否真正考虑过当你执行类似 MyClass lC = MyClass(), lC1; lC1 = lC; 这样的操作时会发生什么? - lapk
@ronag:好的,你说得对 :) 没有真正的理由将其保密。 - moswald
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接