移动构造函数和赋值操作符,使用复制-交换习惯实现



我不明白为什么在下面的例子中,赋值操作符中的参数使用复制构造函数而不是移动构造函数来构建

struct Foo
{
    int data;
    Foo()
    {
        static int cpt = 1;
        data = cpt++;
        std::cout <<  "Foon";
    }
    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

编辑:

from What are move semantics?

"但是如果你说a = x + y, move构造函数会初始化它(因为表达式x + y是右值)…"

但事实上,只有当operator +以"特殊"的方式实现时,x + y才是右值

你的假设不正确。赋值操作符使用可用的构造函数:

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),尽管对于这个简单的类来说没有什么不同。)

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

我不打算说operator +()operator +=()应该如何实现(请参阅其他答案和其中的评论以了解有关此主题的更多信息)。我将关注右值x左值。正如Scott Meyers的 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&(即,对T的左值引用),而类型T的右值被推断为简单的类型T。

您需要为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关于如何"正确地"获取参数的精彩介绍。

最新更新