调用复制 ctor 而不是移动 ctor

Copy ctor called instead of move ctor

为什么从 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 在返回时调用移动构造函数。

在这种情况下有两点需要理解:

  1. a in bar(Alpha &&a) 是命名右值引用;因此,被视为左值。
  2. a还是个参考。

第 1 部分

由于 bar(Alpha &&a) 中的 a 是一个命名的右值引用,它被视为一个左值。将命名右值引用视为左值的动机是为了提供安全性。考虑以下,

Alpha bar(Alpha &&a) {
  baz(a);
  qux(a);
  return a;
}

如果 baz(a)a 视为右值,则可以自由调用移动构造函数,并且 qux(a) 可能无效。该标准通过将命名右值引用视为左值来避免此问题。

第 2 部分

由于a仍然是引用(并且可能引用bar范围之外的对象),bar在返回时调用拷贝构造函数。这种行为背后的动机是提供安全。

参考资料

  1. SO Q&A - return by rvalue reference
  2. Kerrek SB 的评论

是的,非常混乱。我想在这里引用另一个 SO post implicite move。我发现以下评论有点令人信服,

And therefore, the standards committee decided that you had to be explicit about the move for any named variable, regardless of its reference type

其实“&&”已经是放手的意思了,在你做"return"的时候,已经足够安全了,可以做move了。

可能只是标准委员会的选择。

scott meyers 的 "effective modern 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
}

当人们第一次了解右值引用时,这是一个非常非常常见的错误。基本问题是 typevalue category.

之间的混淆

int 是一种类型。 int& 是另一种类型。 int&& 是另一种类型。这些都是不同的类型。

左值和右值是所谓的值类别。请在此处查看精彩图表:What are rvalues, lvalues, xvalues, glvalues, and prvalues?。你可以看到,除了左值和右值,我们还有纯右值和左值和x值,它们形成了各种维恩图类型的关系。

C++ 的规则规定各种类型的变量都可以绑定到表达式。然而,表达式引用类型被丢弃(人们常说表达式没有引用类型)。相反,表达式有一个值类别,它确定哪些变量可以绑定到它。

换句话说:右值引用和左值引用仅在赋值的左侧直接相关,变量是 created/bound。在右侧,我们讨论的是表达式而不是变量,rvalue/lvalue 引用性仅在确定值类别的上下文中相关。

一个非常简单的例子就是简单地查看纯粹类型的事物 int。作为表达式的 int 类型的变量是左值。但是,由计算 returns 和 int 的函数组成的表达式是右值。这对大多数人来说是直观的;关键是要分离表达式的类型(甚至在丢弃引用之前)及其值类别。

这导致的是,即使类型 int&& 的变量只能 绑定 到右值,并不意味着类型 [=12] 的所有表达式=]、个右值。事实上,正如 http://en.cppreference.com/w/cpp/language/value_category 中的规则所说,任何由命名变量组成的表达式始终是左值, 无论类型如何

这就是为什么您需要 std::move 以便将右值引用传递给后续函数,这些函数采用右值引用。这是因为右值引用 而不是 绑定到其他右值引用。它们绑定到右值。如果你想得到移动构造函数,你需要给它一个右值来绑定,一个命名的右值引用不是一个右值。

std::move 是 returns 右值引用的函数。这种表达式的值类别是什么?右值?没有。这是一个xvalue。这基本上是一个右值,具有一些附加属性。

foobar中,表达式a都是一个左值。语句 return a; 表示从初始化程序 a 初始化 return value 对象,并 return 该对象。

这两种情况的区别在于,根据 a 是否在最内层封闭块或函数参数中声明为非易失性自动对象,执行此初始化的重载决议不同。

它适用于 foo 而不是 bar。 (在 bar 中, a 被声明为引用)。所以foo中的return a;选择了移动构造函数来初始化return值,但是bar中的return a;选择了复制构造函数。

全文为C++14 [class.copy]/32:

When the criteria for elision of a copy/move operation are met, but not for an exception-declaration , and the object to be copied is designated by an lvalue, or when the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue. [Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided. —end note ]

其中 "criteria for elision of a copy/move operation are met" 指的是 [class.copy]/31.1:

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

请注意,这些文本将针对 C++17 进行更改。