C++中的“移动”语义使用是否正确?

33
今晚我一直在查看我最近几天工作的一些代码,并开始阅读移动语义,具体来说是std::move。我有几个问题要问你们专家,以确保我走在正确的道路上,不做任何愚蠢的假设!
首先:
1) 最初,我的代码有一个返回大型向量的函数:
template<class T> class MyObject
{
public:
    std::vector<T> doSomething() const;
    {
        std::vector<T> theVector;

        // produce/work with a vector right here

        return(theVector);
    }; // eo doSomething
};  // eo class MyObject

考虑到“theVector”是一时的、“丢弃”的,我修改了该函数:

    std::vector<T>&& doSomething() const;
    {
        std::vector<T> theVector;

        // produce/work with a vector right here

        return(static_cast<std::vector<T>&&>(theVector));
    }; // eo doSomething

这样做是正确的吗?有哪些需要注意的地方呢?

2) 我发现我的一个函数返回std::string时,它自动调用了移动构造函数。在进入字符串的返回(感谢Aragorn),我注意到它调用了一个显式的移动构造函数。为什么string类有移动构造函数而vector没有呢?

我不需要对这个函数进行任何修改以利用移动语义:

// below, no need for std::string&& return value?
std::string AnyConverter::toString(const boost::any& _val) const
{
    string ret;
    // convert here
    return(ret); // No need for static_cast<std::string&&> ?
}; // eo toString

3) 最后,我想进行一些性能测试,所以我得到的惊人快速的结果是因为std::move语义还是我的编译器(VS2010)也进行了一些优化呢?

(为简洁起见省略了_getMilliseconds()的实现)

std::vector<int> v;
for(int a(0); a < 1000000; ++a)
    v.push_back(a);

std::vector<int> x;
for(int a(0); a < 1000000; ++a)
    x.push_back(a);

    int s1 = _getMilliseconds();
std::vector<int> v2 = v;
    int s2 =  _getMilliseconds();
std::vector<int> v3 = std::move(x);
    int s3 =  _getMilliseconds();

    int result1 = s2 - s1;
    int result2 = s3 - s2;

结果显然很棒。result1是一个标准的赋值,用了630毫秒。第二个结果只用了0毫秒。这是这些东西的一个好性能测试吗?

我知道这对你们很多人来说很明显,但在我开始编写代码之前,我想确保我正确理解语义。

提前感谢!


4
你为什么使用static_cast<T&&>而不是std::move - GManNickG
@GMan - 这就是我提出这些问题的原因。我的动力最初来自这里:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm#string,我承认我可能误读了其中的动机,因此发布了这篇文章 :) - Moo-Juice
如果你真的想学习rvalues,我已经在我的答案中添加了一个链接供你阅读。 - GManNickG
@Gman,谢谢你的回复,我打算边喝 Stella Artois 边仔细研究一下。 - Moo-Juice
4个回答

37

引用仍然是引用。就像在C++03中不能返回对局部变量的引用(否则会出现未定义行为)一样,在C++0x中也不行。你最终将得到对已销毁对象的引用,它只是一个右值引用。因此,这是错误的:

std::vector<T>&& doSomething() const
{
    std::vector<T> local;

    return local; // oops
    return std::move(local); // also oops
}

你应该只做你在第二个问题中看到的:

// okay, return by-value 
std::vector<T> doSomething() const
{
    std::vector<T> local;

    return local; // exactly the same as:
    return std::move(local); // move-construct value
}

函数内部的变量在返回时是临时的,因此无需更改任何代码。返回类型负责实现移动语义,而不是您。

如果需要显式移动某些东西(通常情况下不会移动),则可以使用std::move,例如在测试中。 (这似乎很好; 那是在发布版中吗?您应该输出向量的内容,否则编译器将对其进行优化。)

如果想学习右值引用,请阅读此文


4
@Moo: 那么测试就没有意义了。你需要在Release模式下对应用程序进行剖析,最好是在真实的应用程序上。在这种情况下,确实没有必要;移动语义胜出。 - GManNickG
4
我好像记得 return std::move(local) 可以避免复制省略,所以我认为它和 return local 不同,但我不确定是否正确。 - fredoverflow
2
你的链接说,“一个lvalue是一个表达式e,具有这样一个属性:像e = [...];这样的一行代码不会引起编译错误,除非...”,这是错的。例如std::string("hello") = "world"可以完美地编译通过,尽管std::string("hello")是一个rvalue(准确地说是一个prvalue)。它还声称“一个rvalue是一个引用内存位置但不是lvalue的表达式”,但是42并不引用内存位置。坦白说,我不建议从那个链接学习移动语义。 - fredoverflow
1
return local;return std::move(local); 不是相同的。后者会阻止RVO。 - Piotr Skotnicki

14
return(theVector);
由于theVector是一个局部对象,根据特定的语言规则,它已经隐式地发生了移动。请参阅第12.8节第34和35段:
``` 当满足某些条件时,即使类对象的复制/移动构造函数和/或析构函数具有副作用,实现也允许省略类对象的复制/移动构造。 在这种情况下,实现将省略的复制/移动操作的源和目标视为只是两种引用同一对象的不同方式,并且该对象的销毁发生在没有优化的情况下两个对象将被销毁的时间中较晚的那个时间。 这种复制/移动操作的省略称为复制省略,并且可以在以下情况下使用(可以组合以消除多个复制): - 在具有类返回类型的函数中的返回语句中,当表达式是与函数返回类型具有相同cv限定符的非易失自动对象的名称时, 可以通过直接将自动对象构建到函数的返回值中来省略复制/移动操作 [...] - 满足复制操作省略的条件并且要复制的对象由左值指定时,首先执行重载解析以选择用于复制的构造函数,就像该对象由右值指定一样。
请注意,您必须返回std::vector<T>,而不是std::vector<T>&&
但为什么要加括号呢?return不是一个函数:
return theVector;

这主要是习惯使然,我一直这么做。它真的那么糟吗?我有点喜欢它。for()也不是一个函数 ;)(在这里不想引发争论,那大部分只是开玩笑)。 - Moo-Juice
6
@Moo:是的,但 for 需要 括号。 - fredoverflow

7

补充GMan的回答:即使您将返回类型更改为std::vector<T>(没有任何引用,否则将获得UB),您在“1)”中的返回表达式的更改永远不会使性能更好,但可能会使它稍微变差。由于std::vector具有移动构造函数,并且您返回本地对象,无论您写了return theVector;return static_cast<std::vector<T>&&>(theVector);还是return std::move(theVector),都不会调用vector的复制构造函数。在后两种情况下,编译器将被强制调用移动构造函数。但在第一种情况下,如果可以对该函数进行NRVO,则它有自由优化出移动操作。如果由于某种原因不可能进行NRVO,则编译器只能采用调用移动构造函数的方式。因此,如果x是从函数返回的本地非静态对象,则不要将return x;更改为return std::move(x);,否则您将防止编译器使用另一个优化机会。


2
在GMan的回答中补充一下,添加一个评论而不是新的回答。 - Shoe

5

移动对象的标准方式是使用 std::move(x),而不是 static_cast。据我所知,命名返回值优化在通过值返回向量时很可能会启动,因此在移动语义出现之前就已经表现良好。

您的性能测试很好地说明了移动语义对性能的好处:复制赋值必须复制一百万个元素,而移动赋值基本上只是交换向量的内部指针,这是一个微不足道的单词赋值或两个,仅需几个周期。


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