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

34

看下面的示例,返回一个可选的UserName - 一个可移动/可复制的类。

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会导致移动?
这是相关的coliru示例: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...
}

你忘记在示例中删除与问题无关的new和delete操作符了。 - Slava
1
@Slava 删除了删除/新建重载。 - fen
1
更简单的复现代码:std::unique_ptr foo() { std::unique_ptr p; return {p}; } - Slava
感谢@Slava - 我已经更新了问题。 - fen
3个回答

28
因为返回一个自动存储期对象的名称会被视为返回该对象的rvalue。请注意,这仅适用于返回语句中的表达式是一个(可能带括号但不包括大括号)名称的情况,例如return u;return (u);,所以return {u};像通常一样工作,即调用复制构造函数。
标准中相关部分为[class.copy.elision]/3
在以下复制初始化上下文中,移动操作可能会代替复制操作:
  • 如果返回语句中的表达式是内层最近的函数或lambda表达式的参数声明或主体中声明的具有自动存储期的对象的(可能带括号的)id-expression,或
  • ...
重载解析以选择复制的构造函数首先按照对象被指定为rvalue的方式执行。

我不明白为什么return{u}没有得到与其他语句相同的处理方式,我的意思是它是一个返回语句,变量“u”正在超出范围,是否有情况应该使用复制? - watashiSHUN
@watashiSHUN 我不知道。这是规定。我认为只有制定这个规定的委员会知道原因。 - xskxzr

5
这是一种称为“大括号初始化列表”的东西。[dcl.init.list]/1.3 更具体地说,它是一个“表达式或大括号初始化列表”。[dcl.init]/1 属于“返回语句”的一种[stmt.return]/2
在除了返回类型不是cv void的函数中,使用任何其他操作数的返回语句只能通过从操作数进行“复制初始化”,来初始化(显式或隐式)函数调用的glvalue结果或prvalue结果对象。

从这一点开始,让我引用xskxzr的答案,提到[class.copy.elision]/3

在以下复制初始化上下文中,可能会使用移动操作代替复制操作:

  • 如果返回语句中的表达式([stmt.return])是一个(可能带括号的)id表达式,该表达式命名了具有自动存储期限的对象,在最内层封闭函数或lambda表达式的主体或参数声明子句中声明,或者

在通俗易懂的语言中,之所以调用复制而不是移动,是因为花括号初始化列表调用了恰好是左值的u
因此,你可能想知道如果花括号初始化列表调用的是rvalueu...
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

1

返回 { arg1, arg2, ... } ;

复制列表初始化。通过复制初始化进行复制列表初始化,从初始化器列表初始化(return)对象。


1
但是return u;也是一种复制初始化,正如复制初始化 - cppreference.com所述。 - fen
@fen 复制初始化与复制列表初始化不同。 - Caleth
这实际上并没有回答问题。是的,return {u}; 是复制列表初始化。没错。但是,为什么这个会复制而另一个会移动呢? - Barry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接