为什么从bar
返回时调用复制构造函数而不是移动构造函数?
#include <iostream>
using namespace std;
class Alpha {
public:
Alpha() { cout << "ctor" << endl; }
Alpha(Alpha &) { cout << "copy ctor" << endl; }
Alpha(Alpha &&) { cout << "move ctor" << endl; }
Alpha &operator=(Alpha &) { cout << "copy asgn op" << endl; }
Alpha &operator=(Alpha &&) { cout << "move asgn op" << endl; }
};
Alpha foo(Alpha a) {
return a; // Move ctor is called (expected).
}
Alpha bar(Alpha &&a) {
return a; // Copy ctor is called (unexpected).
}
int main() {
Alpha a, b;
a = foo(a);
a = foo(Alpha());
a = bar(Alpha());
b = a;
return 0;
}
如果bar
执行return move(a)
,则行为与预期的一样。我不明白为什么std::move
的调用是必要的,因为foo
在返回时调用了move构造函数。
在这种情况下有两件事需要理解:
bar(Alpha &&a)
中的a
是一个命名的右值引用;因此,被视为左值- CCD_ 8仍然是一个参考
第1部分
由于bar(Alpha &&a)
中的a
是一个命名的右值引用,因此它被视为一个左值。将命名的右值引用视为左值的动机是为了提供安全性。考虑以下内容,
Alpha bar(Alpha &&a) {
baz(a);
qux(a);
return a;
}
如果baz(a)
将a
视为右值,则可以自由调用移动构造函数,并且qux(a)
可能无效。该标准通过将命名的右值引用视为左值来避免这个问题。
第2部分
由于a
仍然是一个引用(并且可能引用bar
范围之外的对象),因此bar
在返回时调用复制构造函数。这种行为背后的动机是为了提供安全。
参考
- SO问答;通过右值引用返回
- Kerrek SB评论
是的,非常令人困惑。我想在这里引用另一个SO的帖子。我发现以下评论有点令人信服,
因此,标准委员会决定明确任何命名变量的移动,无论其参考型
实际上"&&"已经表示放手,当你"返回"时,移动是足够安全的。
也许这只是标准委员会的选择。
scott-meyers的《有效的现代c++》第25条也对此进行了总结,但没有给出太多解释。
Alpha foo() {
Alpha a
return a; // RVO by decent compiler
}
Alpha foo(Alpha a) {
return a; // implicit std::move by compiler
}
Alpha bar(Alpha &&a) {
return a; // Copy ctor due to lvalue
}
Alpha bar(Alpha &&a) {
return std:move(a); // has to be explicit by developer
}
当人们第一次了解右值引用时,这是一个非常常见的错误。基本问题是类型和值类别之间的混淆。
CCD_ 17是一种类型。CCD_ 18是另一种类型。CCD_ 19是另一种类型。这些都是不同的类型。
左值和右值是被称为值范畴的东西。请查看这里的奇妙图表:什么是rvalues、lvalues、xvalues、glvalues和prvalues?。你可以看到,除了lvalues和rvalues,我们还有prvalues、glvalues和xvalues,它们形成了各种venn图类型的关系。
C++有一些规则,规定各种类型的变量可以绑定到表达式。然而,表达式引用类型会被丢弃(人们经常说表达式没有引用类型)。相反,表达式有一个值类别,它决定哪些变量可以绑定到它
换句话说:右值引用和左值引用仅在赋值的左侧直接相关,即正在创建/绑定的变量。在右边,我们谈论的是表达式而不是变量,右值/左值参考性仅在确定值类别的上下文中相关。
一个非常简单的例子是简单地观察纯类型int
的事物。作为表达式的int
类型的变量是左值。然而,由计算返回int
的函数组成的表达式是右值。这对大多数人来说是直观的;不过,关键是要区分表达式的类型(甚至在丢弃引用之前)及其值类别。
这导致的结果是,即使int&&
类型的变量只能绑定到右值,也不意味着所有int&&
、类型的表达式都是右值。事实上,按照http://en.cppreference.com/w/cpp/language/value_category比如说,任何由命名变量组成的表达式,无论类型为,都始终是一个左值。
这就是为什么您需要std::move
,以便将右值引用传递到通过右值引用的后续函数中。这是因为右值引用不绑定到其他右值引用。它们与右价结合。如果你想获得move构造函数,你需要给它一个要绑定的右值,而命名的右值引用不是右值。
std::move
是一个返回右值引用的函数。这种表达的价值范畴是什么?一个右价?没有。这是一个x值。这基本上是一个右值,带有一些附加属性。
在foo
和bar
中,表达式a
都是左值。语句return a;
意味着从初始化器a
初始化返回值对象,并返回该对象。
这两种情况之间的区别在于,根据a
是否被声明为最内部封装块内的非易失性自动对象或函数参数,执行该初始化的过载解析是不同的。
这是针对foo
而不是针对bar
的。(在bar
中,a
被声明为引用)。因此,foo
中的return a;
选择移动构造函数来初始化返回值,但bar
中的return a;
选择复制构造函数。
全文为C++14[class.copy]/32:
当满足省略复制/移动操作的标准,但不满足异常声明的标准,并且要复制的对象由左值指定时,或者,当return语句中的表达式是一个(可能加了括号)id表达式,该表达式命名的对象具有在最内部的封闭函数或lambda表达式的body或参数声明子句中声明的自动存储持续时间时,将首先执行为副本选择构造函数的重载解析,就好像该对象是由右值指定的一样。如果第一个重载解析失败或没有执行,或者如果所选构造函数的第一个参数的类型不是对对象类型的右值引用(可能是cv限定的),则会再次执行重载解析,将对象视为左值。[注意:无论是否会发生复制省略,都必须执行这种两阶段的重载解析。它确定了如果不执行省略将调用的构造函数,并且即使调用被省略,所选的构造函数也必须是可访问的。--end Note]
其中"符合省略复制/移动操作的标准"指的是[class.copy]/31.1:
- 在具有类返回类型的函数中的返回语句中,当表达式是与函数返回类型具有相同cv不合格类型的非易失性自动对象(函数或catch子句参数除外)的名称时,可以通过构造将自动对象直接转换为函数的返回值
注意,这些文本将在C++17中更改。