移动构造函数和赋值运算符使用copy-and-swap惯用法实现

时间:2013-04-02 08:03:02

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

我在下面的例子中不明白为什么assignement运算符中的参数使用复制构造函数而不是构建的移动构造函数

struct Foo
{
    int data;

    Foo()
    {
        static int cpt = 1;
        data = cpt++;
        std::cout <<  "Foo\n";
    }

    Foo(const Foo& foo)
    {
        std::cout <<  "const Foo&\n";
        data = foo.data; 
    }

    Foo(Foo&& foo)
    {
        std::cout <<  "Foo&&\n";
        data = foo.data;
        foo.data = 0;
    }

    Foo& operator= (Foo foo)  //<--- call the copy ctor and not the move ctor as I expected
    {
        data = foo.data;
    std::cout <<  "operator=\n";
    return *this;   
    }

    Foo& operator+ (const Foo& foo)
    {
        data += foo.data;
        return *this;
    }
};


int main()
{ 
    Foo f;
    Foo f1;
    Foo f3;

    f3 = f + f1;

    std::cout << f3.data;

    std::cin.ignore(); 

    return 1; 
}

输出:

Foo
Foo
Foo
const Foo&
operator=
3

编译器:MSVCS2012 CTP

编辑:

来自What are move semantics?

  

&#34;但是如果你说a = x + y,那么移动构造函数会初始化它   (因为表达式x + y是一个右值)...&#34;

但事实上,只有{&#34;特殊&#34;}中实现operator +时,x + y才是右值。怎么样?

3 个答案:

答案 0 :(得分:3)

您的假设不正确。赋值运算符使用可用的构造函数:

Foo a;
Foo b;

Foo x;

x = a;             // copy constructor
x = std::move(b);  // move constructor
x = Foo();         // move constructor

你的误解来自你的笨拙operator+,它不会像你可能会相信的那样返回右值。

更新:正确的“加号”运算符对如下所示:

Foo& operator+= (const Foo& foo)
{
    data += foo.data;
    return *this;
}

Foo operator+ (const Foo& rhs)
{
    return Foo(*this) += rhs;
}

(一般情况下,您也可以制作这些签名Foo rhs并在适当的时候使用std::move(rhs),但这对于这个简单的类没有任何区别。)

答案 1 :(得分:1)

正如其他人所指出的那样,问题是operator +()。具体来说,它返回Foo&,即左值引用。由于左值引用是左值(这不是重言式),因此它不能绑定到右值引用。因此,无法调用Foo(Foo&&)

我不打算说明operator +()operator +=()应该如何实施(有关此主题的更多信息,请参阅其他答案及其中的评论)。我将专注于 rvalue x lvalue 。正如斯科特迈耶斯的Universal References in C++11

所述
  

这些术语的精确定义很难发展(   C ++ 11标准通常指定表达式是否为左值   或根据具体情况逐步估算......)

但是,以下函数可用于检查对象是左值还是左值:

#include <type_traits>

template <typename T>
bool is_lvalue(T&&) {
    return std::is_reference<T>::value;
}

template <typename T>
bool is_rvalue(T&&) {
    return !std::is_reference<T>::value;
}

例如,以下代码

struct bar {
    bar&  operator+(const bar&) { return *this;  }
    bar&& operator-(const bar&) { return std::move(*this); }
};

int main() {

    bar b1;
    bar b2;

    std:: cout << std::boolalpha;

    std:: cout << is_rvalue( b1 + b2) << std::endl;
    std:: cout << is_rvalue( b1 - b2) << std::endl;

}

输出

false
true

注意:bar受到原始代码的启发,使用operator +()operator -(),但就此而言,作为运营商没有什么特别之处。

可以改进函数is_lvalue()is_rvalue()。例如,它们可以是constexrp,但我在这里保持简单,因为有些编译器(特别是VS2012)尚未实现constexpr

关于这些功能如何工作的解释基于推断T的方式。我引用前面提到的Scott Meyers的文章:

  

在模板参数[...]的类型推导期间,推导出相同类型的左值和右值具有略微不同的类型。特别地,类型T的左值被推断为T&amp;类型的左值。 (即,左值引用T),而类型T的rvalues推断为简单的T型。

答案 2 :(得分:0)

您需要为operator=提供两次重载,因为您的代码不会移动data

Foo& operator= (const Foo& foo)
{
    data = foo.data;
    return *this;   
}

// only needed if the implementation can benefit from a movable object
Foo& operator= (Foo&& foo)
{
    data = std::move(foo.data); // without std::move, as above, the copy-ctor is called
    return *this;   
}

那就是说,签名有什么问题:

Foo& operator= (Foo foo);

?问题是,您总是强制要传递的对象,这可能导致效率低下。考虑:

Foo f, f2;
f = f2;

(当然,在示例中,优化器很可能会删除所有开销)。

通常,operator=需要副本,因此不应该请求副本。另见Andy Prowl对how to take parameters "correctly"的好评。