【问题标题】:move constructor and assignment operator impletemented using copy-and-swap idiom使用复制和交换习语实现的移动构造函数和赋值运算符
【发布时间】:2013-04-02 08:03:02
【问题描述】:

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

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?

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

但事实上 x + y 只有在 operator + 以“特殊”方式实现时才是右值,所以?

【问题讨论】:

  • 回答“但实际上 x + y 只有在 operator + 以“特殊”方式实现时才为右值,所以?”:是的,没错。请参阅我的回答,特别是使用 bar 及其两个运算符 + 和 - 的代码。

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


【解决方案1】:

你的假设是错误的。赋值运算符使用任何可用的构造函数:

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+呢?
  • @Guillaume07:让它返回一个Foo
  • @Guillaume07:更好的是,实现operator+=,它应该返回Foo&amp;,并以operator+=为基础operator+。请参阅我的两个库(Boost.Operators 和移动感知 df.operators)的个人资料,它们可以帮助您避免错误并安全地一遍又一遍地编写相同的样板代码:)
  • @Kerrek:我试图将operator+ 的返回值从Foo&amp; 切换到Foo,但同样的事情发生了,copy ctor 被调用,知道吗?
  • @DanielFrey:老实说,除非分析器特别指出,否则我不会担心这种级别的微优化。 Boost 担心它是正常的,因为如果它的用户因为效率低下而绕过它,他们会感到失望,但这并不意味着您应该像希望达到相同的效率水平那样“混淆”您的代码...除非它真的很重要。
【解决方案2】:

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

我不会说应该如何实现operator +()operator +=()(有关此主题的更多信息,请参阅其他答案和其中的 cmets)。我将专注于rvalue x lvalue。正如 Scott Meyers 所说的 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& 类型(即 T 的左值引用),而 T 类型的右值被推导为 T 类型。

【讨论】:

    【解决方案3】:

    您需要为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" 的精彩演绎。

    【讨论】:

    • 不,没必要。其实OP的代码要好很多。
    • @KerrekSB 即使对于省略了foo 副本的可移动参数,OP 仍然调用data = foo.data,它始终是一个副本。您忘记将移动传递给成员。 (好吧,在这个例子中,它只是一个int,但我一般在想)此外,代码并不总是更好,因为它强制复制foo,即使它根本不需要复制,例如f3 = f;.
    猜你喜欢
    • 2011-06-09
    • 2017-01-16
    • 2012-06-27
    • 2019-09-29
    • 2011-05-19
    • 2011-07-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多