为什么返回 std::optional 有时会移动有时会复制?

Why does returning a std::optional sometimes move and sometimes copy?

参见下面返回可选的 UserName 的示例 - a movable/copyable class.

std::optional<UserName> CreateUser()
{
   UserName u;
   return {u}; // this one will cause a copy of UserName
   return u;   // this one moves UserName
}


int main()
{
   auto d = CreateUser();
}

为什么 return {u} 导致复制并 return u 移动?

这是相关的大肠杆菌样本:http://coliru.stacked-crooked.com/a/6bf853750b38d110

另一种情况(感谢@Slava的评论):

std::unique_ptr<int> foo() 
{ 
    std::unique_ptr<int> p; 
    return {p};  // uses copy of unique_ptr and so it breaks...
}

因为 returning 具有自动存储持续时间的对象的 name 被视为 returning 对象的右值。请注意,仅当 return 语句中的表达式是一个(可能带括号,不包括大括号)名称时才有效,例如 return u;return (u);,因此 return {u}; 照常工作,即调用复制构造函数。

标准中的相关部分[class.copy.elision]/3:

In the following copy-initialization contexts, a move operation might be used instead of a copy operation:

  • If the expression in a return statement ([stmt.return]) 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, or
  • ...

overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue.

return { arg1, arg2, ... } ;

copy-list-initialization。 (return) 对象通过复制列表初始化的复制初始化从初始化列表初始化

这是一种 braced-init-list[dcl.init.list]/1.3

更具体地说,它是一个 " expr-or-braced-init-list [dcl.init]/1

return 语句 " [stmt.return]/2

A return statement with any other operand shall be used only in a function whose return type is not cv void; the return statement initializes the glvalue result or prvalue result object of the (explicit or implicit) function call by copy-initialization from the operand.

从这点来说,让我引用xskxzr提到[class.copy.elision]/3

的回答

In the following copy-initialization contexts, a move operation might be used instead of a copy operation:

  • If the expression in a return statement ([stmt.return]) 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, or

用普通人的话来说,调用copy而不是move的原因是braced-init-list调用u恰好是左值。

所以,你可能想知道 braced-init-list 是否调用 urvalue ...

return {std::move(u)};

好吧,u 被移动到一个新的右值 UserName 并且复制省略立即生效。

所以这需要一步

return u;

godbolt.org/g/b6stLr

wandbox.org/permlink/7u1cPc0TG9gqToZD

#include <iostream>
#include <optional>

struct UserName
{
  int x;
  UserName() : x(0) {};
  UserName(const UserName& other) : x(other.x) { std::cout << "copy " << x << "\n"; };
  UserName(UserName&& other)      : x(other.x) { std::cout << "move "  << x << "\n"; };
};

std::optional<UserName> CreateUser()
{
  UserName u;
  return u;   // this one moves UserName
}

std::optional<UserName> CreateUser_listinit()
{
  UserName u;
  auto whatever{u};
  return whatever;
}

std::optional<UserName> CreateUser_listinit_with_copy_elision()
{
  UserName u;
  return {u};
}

std::optional<UserName> CreateUser_move_listinit_with_copy_elision()
{
  UserName u;
  return {std::move(u)};
}

int main()
{
  std::cout << "CreateUser() :\n";
  [[maybe_unused]] auto d = CreateUser();

  std::cout << "\nCreateUser_listinit() :\n";
  [[maybe_unused]] auto e = CreateUser_listinit();

  std::cout << "\nCreateUser_listinit_with_copy_elision() :\n";
  [[maybe_unused]] auto f = CreateUser_listinit_with_copy_elision();

  std::cout << "\nCreateUser_move_listinit_with_copy_elision() :\n";
  [[maybe_unused]] auto g = CreateUser_move_listinit_with_copy_elision();
}

打印

CreateUser() :
move 0

CreateUser_listinit() :
copy 0
move 0

CreateUser_listinit_with_copy_elision() :
copy 0

CreateUser_move_listinit_with_copy_elision() :
move 0