c++ 11右值和移动语义混淆(返回语句)

我试图理解右值引用和移动语义的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();
132576 次浏览

它们都不会复制,但第二个会指向一个已销毁的向量。命名右值引用在常规代码中几乎不存在。你写它就像用c++ 03写副本一样。

std::vector<int> return_vector()
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}


std::vector<int> rval_ref = return_vector();

只是现在,矢量被移动了。类的用户在绝大多数情况下不处理它的右值引用。

这些都不会做额外的复制。即使不使用RVO,我相信,新标准说移动构造在做返回时更倾向于复制。

我相信您的第二个示例会导致未定义的行为,因为您返回了对局部变量的引用。

第一个例子

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();

你的第三个例子与第一个例子大致相同。tmp上的std::move是不必要的,实际上可能会导致性能下降,因为它会抑制返回值优化。

编写代码的最佳方式是:

最佳实践

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,并且返回的类型没有移动构造函数时,才会使用复制构造函数进行返回。

简单的答案是,您应该像编写常规引用代码一样为右值引用编写代码,并且在99%的时间内,您应该在精神上对它们进行相同的处理。这包括所有关于返回引用的旧规则(即永远不要返回对局部变量的引用)。

除非你正在编写一个模板容器类,需要利用std::forward,并且能够编写一个接受左值或右值引用的泛型函数,否则这或多或少是正确的。

move构造函数和move赋值函数的最大优点之一是,如果你定义了它们,编译器可以在RVO(返回值优化)和NRVO(命名返回值优化)调用失败的情况下使用它们。这对于返回昂贵的对象(如容器)来说是相当大的。从方法中有效地按值字符串。

右值引用的有趣之处在于,你也可以将它们用作普通函数的参数。这允许你编写具有const引用重载的容器(const foo&Other)和右值引用(food &&其他)。即使参数太笨拙,不能通过简单的构造函数调用传递,也可以这样做:

std::vector vec;
for(int x=0; x<10; ++x)
{
// automatically uses rvalue reference constructor if available
// because MyCheapType is an unamed temporary variable
vec.push_back(MyCheapType(0.f));
}




std::vector vec;
for(int x=0; x<10; ++x)
{
MyExpensiveType temp(1.0, 3.0);
temp.initSomeOtherFields(malloc(5000));


// old way, passed via const reference, expensive copy
vec.push_back(temp);


// new way, passed via rvalue reference, cheap move
// just don't use temp again,  not difficult in a loop like this though . . .
vec.push_back(std::move(temp));
}

STL容器已经更新到几乎任何东西(散列键和值,向量插入等)都有移动重载,这是你会看到它们最多的地方。

也可以将它们用于普通函数,如果只提供右值引用参数,则可以强制调用者创建对象并让函数执行移动操作。这更像是一个例子,而不是真正的好用法,但在我的呈现库中,我为所有加载的资源分配了一个字符串,以便更容易看到每个对象在调试器中代表什么。界面是这样的:

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
tex->friendlyName = std::move(friendlyName);
return tex;
}

这是一种“漏洞抽象”的形式,但允许我利用我必须在大多数时间内创建字符串的事实,并避免再次复制它。这并不是真正的高性能代码,但它是一个很好的例子,说明了人们在掌握该特性时的各种可能性。这段代码实际上要求变量要么是调用的临时变量,要么调用std::move:

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

但是这不会编译!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

不是一个答案本身,而是一个指导方针。大多数情况下,声明局部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&&,它是一个non-temporary对象的引用,你可以移动到你的对象。

关于RVO:这些机制通常工作,编译器可以很好地避免复制,但在返回路径不明显的情况下(异常,if条件决定你将返回的命名对象,可能还有其他一些),引用是你的救星(即使可能更昂贵)。

正如在第一个答案的注释中已经提到的,return std::move(...);结构可以在返回局部变量以外的情况下产生不同。下面是一个可运行的例子,它记录了当你返回一个带有或不带有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;
}

据推测,return std::move(some_member);只在你真正想要移动特定的类成员时才有意义,例如,在这种情况下,class C表示存在时间较短的适配器对象,其唯一目的是创建struct A的实例。

注意struct A总是从class B中得到复制,即使class B对象是一个r值。这是因为编译器没有办法告诉class Bstruct A实例将不再被使用。在class C中,编译器确实从std::move()获得了这个信息,这就是为什么struct A得到class B0,除非class C的实例是常量。