【问题标题】:Implementing copy assignment operator in terms of move constructor根据移动构造函数实现复制赋值运算符
【发布时间】:2017-01-16 02:34:34
【问题描述】:

考虑以下概念/示例类

class Test
{
public:
    explicit Test(std::string arg_string)
        : my_string( std::move(arg_string) )
    { }

    Test(const Test& Copy) {
        this->my_string = Copy.my_string;
    }

    Test& operator=(Test Copy) {
        MoveImpl( std::move(Copy) );
        return *this;
    }

    Test(Test&& Moved) {
        MoveImpl( std::forward<Test&&>(Moved) );
    }

    Test& operator=(Test&& Moved) {
        MoveImpl( std::forward<Test&&>(Moved) );
        return *this;
    }

private:
    void MoveImpl(Test&& MoveObj) {
        this->my_string = std::move(MoveObj.my_string);
    }

    std::string my_string;
};

复制构造函数像往常一样采用const&amp;

复制赋值运算符是根据复制构造函数实现的(如果我没记错的话,Scott Meyers 指出异常安全和自赋值问题是通过这种方式解决的)。

在实现移动构造函数和移动赋值运算符时,我发现存在一些“代码重复”,我通过添加 MoveImpl(&amp;&amp;) 私有方法“消除”了这些。

我的问题是,由于我们知道复制赋值运算符获取了将在作用域结束时清理的对象的新副本,因此使用 MoveImpl() 函数来实现复制赋值运算符。

【问题讨论】:

  • 这段代码真的有效吗?移动分配对我来说似乎是一个模棱两可的重载(例如在 Test() = Test(); 中)。
  • 题外话:你为什么不在复制构造函数中使用成员初始化列表,或者为什么不只是= default;呢?实际上,除了第一个构造函数之外,每个构造函数都进行了赋值,这不好
  • std::forward&lt;Test&amp;&amp;&gt;(Moved) 可以简单地为std::move(Moved)
  • @KerrekSB,不,你是对的,确实代码抱怨重载解决方案。如果我将复制分配声明为Test&amp; operator=(const Test&amp; Copy),错误就会消失。但是,我认为这会带来自分配和异常安全问题。
  • @LogicStuff,如果MoveImpl 执行移动,移动构造函数如何执行赋值?

标签: c++ c++11 move-semantics


【解决方案1】:

复制赋值运算符的按值签名的美妙之处在于它消除了对移动赋值运算符的需要(前提是您正确定义了移动构造函数!)。

class Test
{
public:
    explicit Test(std::string arg_string)
        : my_string( std::move(arg_string) )
    { }

    Test(const Test& Copy)
        : my_string(Copy.my_string)
    { }

    Test(Test&& Moved)
        : my_string( std::move(Moved.my_string) )
    { }

    // other will be initialized using the move constructor if the actual
    // argument in the assignment statement is an rvalue
    Test& operator=(Test other)
    {
        swap(other);
        return *this;
    }

    void swap(Test& other)
    {
        std::swap(my_string, other.my_string);
    }

private:
    std::string my_string;
};

【讨论】:

  • 我明白了,通过获取赋值运算符的副本,您可以让编译器决定调用哪个构造函数,这样您就不必编写 2 个不同的赋值运算符。
【解决方案2】:

您的想法是正确的,但共同点在于交换操作。

如果你早点尝试这样做,你将失去在构造函数的初始化列表中初始化成员的机会,这在概念上会导致成员的冗余默认初始化和整齐地处理异常的困难。

这更接近您所追求的模型:

class Test
{
public:
    explicit Test(std::string arg_string)
    : my_string( std::move(arg_string) )
    { }

    Test(const Test& Copy) : my_string(Copy.my_string)
    {
    }

    Test& operator=(Test const& Copy)
    {
        auto tmp(Copy);
        swap(tmp);
        return *this;
    }

    Test(Test&& Moved) : my_string(std::move(Moved.my_string))
    {
    }

    Test& operator=(Test&& Moved)
    {
        auto tmp = std::move(Moved);
        swap(tmp);
        return *this;
    }

    void swap(Test& other) noexcept
    {
        using std::swap;
        swap(my_string, other.my_string);
    }

private:

    std::string my_string;
};

当然,实际上,零规则应始终优先考虑,除非您绝对需要在析构函数中进行特殊处理(您几乎从不这样做):

class Test
{
public:
    explicit Test(std::string arg_string)
    : my_string( std::move(arg_string) )
    { }

// copy, assignment, move and move-assign are auto-generated
// as is destructor

private:

    std::string my_string;
};

【讨论】:

    猜你喜欢
    • 2011-06-09
    • 2016-05-19
    • 2015-03-03
    • 2013-06-11
    • 2012-06-27
    • 1970-01-01
    • 1970-01-01
    • 2019-09-29
    • 2011-05-19
    相关资源
    最近更新 更多