我试图理解右值引用和移动语义的c++ 11。
这些例子之间的区别是什么,它们中的哪一个不会做矢量复制?
第一个例子
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
第二个例子
std::vector<int>&& return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
第三个例子
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
正如在第一个答案的注释中已经提到的,返回std::move(…);除了返回局部变量之外,Construct可以产生不同的情况。下面是一个可运行的例子,它记录了当你返回一个带有或不带有std::move()的成员对象时会发生什么:
#include <iostream>
#include <utility>
struct A {
A() = default;
A(const A&) { std::cout << "A copied\n"; }
A(A&&) { std::cout << "A moved\n"; }
};
class B {
A a;
public:
operator A() const & { std::cout << "B C-value: "; return a; }
operator A() & { std::cout << "B L-value: "; return a; }
operator A() && { std::cout << "B R-value: "; return a; }
};
class C {
A a;
public:
operator A() const & { std::cout << "C C-value: "; return std::move(a); }
operator A() & { std::cout << "C L-value: "; return std::move(a); }
operator A() && { std::cout << "C R-value: "; return std::move(a); }
};
int main() {
// Non-constant L-values
B b;
C c;
A{b}; // B L-value: A copied
A{c}; // C L-value: A moved
// R-values
A{B{}}; // B R-value: A copied
A{C{}}; // C R-value: A moved
// Constant L-values
const B bc;
const C cc;
A{bc}; // B C-value: A copied
A{cc}; // C C-value: A copied
return 0;
}
假设,返回std::move(some_member);只有当你真的想要移动特定的类成员时才有意义,例如,在类C表示寿命较短的适配器对象的情况下,它的唯一目的是创建结构a的实例。
注意结构A总是从类B复制出来,即使类B对象是r值。这是因为编译器没有办法告诉类B的结构体A的实例不再被使用。在类C中,编译器确实从std::move()中获得了这个信息,这就是为什么结构A会被移动,除非类C的实例是常量。
第一个例子
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
第一个示例返回一个由rval_ref捕获的临时对象。该临时对象的生命周期将超出rval_ref定义,您可以使用它,就好像您已经按值捕获了它一样。这与以下内容非常相似:
const std::vector<int>& rval_ref = return_vector();
除了在我的重写中,你显然不能以非const的方式使用rval_ref。
第二个例子
std::vector<int>&& return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
在第二个示例中,您创建了一个运行时错误。Rval_ref现在在函数中保存了对已销毁TMP的引用。运气好的话,这段代码会立即崩溃。
第三个例子
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
你的第三个例子与第一个例子大致相同。std::move在tmp上是不必要的,实际上可能会导致性能下降,因为它会抑制返回值优化。
编写代码的最佳方式是:
最佳实践
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> rval_ref = return_vector();
例如,就像在c++ 03中一样。TMP在return语句中被隐式地视为右值。它将通过返回值优化(不复制,不移动)返回,或者如果编译器决定它不能执行RVO,那么它将使用vector的move构造函数来返回。只有当不执行RVO,并且返回的类型没有移动构造函数时,才会使用复制构造函数进行返回。
不是答案本身,而是一个指导方针。大多数情况下,声明局部T&&变量没有太大意义(就像使用std::vector<int>&& rval_ref一样)。你仍然需要std::move()它们才能在foo(T&&)类型方法中使用。还有一个已经提到的问题,当你试图从函数返回这样的rval_ref时,你会得到标准的reference-to destroyed-temporary-fiasco。
大多数情况下,我会采用以下模式:
// Declarations
A a(B&&, C&&);
B b();
C c();
auto ret = a(b(), c());
你不持有任何对返回的临时对象的引用,因此你避免了(没有经验的)程序员希望使用移动对象的错误。
auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));
// Either these just fail (assert/exception), or you won't get
// your expected results due to their clean state.
bRet.foo();
cRet.bar();
显然,在某些情况下(尽管相当罕见),函数会真正返回一个T&&,它是一个对非临时对象的引用,您可以将其移动到您的对象中。
关于RVO:这些机制通常工作,编译器可以很好地避免复制,但在返回路径不明显的情况下(异常,如果条件决定你将返回的命名对象,可能还有其他),引用是你的救星(即使可能更昂贵)。