是否有必要在 return 语句中使用 std::move,是否应该返回右值引用?

Is it necessary to std::move in a return statement, and should you return rvalue references?

提问人:Tarantula 提问时间:2/14/2011 最后编辑:Jan SchultkeTarantula 更新时间:9/28/2023 访问量:139120

问:

我正在尝试理解右值引用并移动 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();
C C++11 移动语义 rvalue-reference c++-faq

评论

61赞 fredoverflow 2/14/2011
请不要通过引用返回局部变量。右值引用仍是引用。
82赞 Tarantula 2/15/2011
这显然是故意的,以便理解示例之间的语义差异,哈哈
0赞 3Dave 12/23/2013
@FredOverflow老问题,但我花了一秒钟才理解你的评论。我认为 #2 的问题是是否创建了一个持久的“副本”。std::move()
8赞 fredoverflow 12/23/2013
@DavidLively 不会创建任何内容,它只是将表达式转换为 xvalue。在计算过程中不会复制或移动任何对象。std::move(expression)std::move(expression)
1赞 Jan Schultke 9/28/2023
@TylerH如果您有更好的标题,请提供一个,而不是简单地回滚到“C++:有些东西混淆” 这样的标题不明确,使用标题内标记,因此违反了 SO 建议。回滚不应该轻率地回滚改进,相反,编辑应该始终提高帖子的质量,理想情况下解决所有问题,而不是留下一些未解决的问题,更不用说引入旧问题了。

答:

48赞 Puppy 2/14/2011 #1

它们都不会复制,但第二个将引用被破坏的向量。命名右值引用几乎从来不存在于常规代码中。你写它就像你在 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();

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

评论

0赞 Tarantula 2/14/2011
你真的确定第三个例子要做向量复制吗?
0赞 Puppy 2/14/2011
@Tarantula:它会破坏你的矢量。在破解之前它是否复制了它并不重要。
4赞 fredoverflow 2/14/2011
我看不出你提议的破坏有任何理由。将局部右值引用变量绑定到右值是完全可以的。在这种情况下,临时对象的生存期将延长到右值引用变量的生存期。
2赞 Mark Lakata 12/11/2014
只是澄清一点,因为我正在学习这个。在这个新示例中,向量没有被移动到 中,而是使用 RVO 直接写入(即复制省略)。和复制省略是有区别的。A 可能仍涉及一些需要复制的数据;在向量的情况下,实际上在复制构造函数中构造了一个新的向量并分配了数据,但数据数组的大部分只是通过复制指针来复制(本质上)。复制省略避免了所有副本的 100%。tmprval_refrval_refstd::movestd::move
0赞 Daniel Langr 2/20/2018
@MarkLakata 这是 NRVO,不是 RVO。NRVO 是可选的,即使在 C++17 中也是如此。如果未应用,则使用 的 移动构造函数构造返回值和变量。没有涉及 / 不涉及 . 在这种情况下,在语句中被视为右值rval_refstd::vectorstd::movetmpreturn
3赞 Edward Strange 2/14/2011 #2

这些都不会进行任何额外的复制。即使不使用 RVO,我相信新标准也表明,在进行退货时,移动结构比复制更可取。

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

659赞 Howard Hinnant 2/14/2011 #3

第一个例子

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_refrval_ref

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

除了在我的重写中,您显然不能以非常规的方式使用。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();

在第二个示例中,您创建了一个运行时错误。 现在在函数中保存对 destructed 的引用。如果运气好的话,这段代码会立即崩溃。rval_reftmp

第三个例子

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

第三个示例大致等同于第一个示例。on 是不必要的,实际上可能是一种性能悲观,因为它会抑制返回值优化。std::movetmp

对你正在做的事情进行编码的最好方法是:

最佳实践

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 中一样。 在 return 语句中隐式视为右值。它要么通过返回值优化(不复制,不移动)返回,要么如果编译器决定它不能执行 RVO,那么它将使用 vector 的移动构造函数来执行返回。仅当未执行 RVO 并且返回的类型没有移动构造函数时,复制构造函数才会用于返回。tmp

评论

78赞 Howard Hinnant 2/26/2013
当您按值返回本地对象时,编译器将 RVO,并且本地的类型和函数的返回是相同的,并且都不是 cv 限定的(不返回 const 类型)。不要返回条件 (:?) 语句,因为它会抑制 RVO。不要将本地函数包装在返回对本地函数的引用的其他函数中。只。多个 return 语句是可以的,并且不会禁止 RVO。return my_local;
39赞 boycy 2/26/2013
有一点需要注意:当返回本地对象的成员时,移动必须是显式的。
5赞 Howard Hinnant 2/28/2013
@NoSenseEtAl:回车线上没有临时创建。 不会创建临时的。它将左值转换为x值,不做任何副本,不创造任何东西,不破坏任何东西。该示例与通过 lvalue-reference 返回并从返回行中删除 的情况完全相同:无论哪种方式,您都得到了对函数内部局部变量的悬空引用,并且该变量已被销毁。movemove
19赞 Deduplicator 7/30/2014
“多个 return 语句是可以的,不会禁止 RVO”:仅当它们返回相同的变量时。
5赞 Howard Hinnant 7/30/2014
@Deduplicator:你是对的。我说得没有我想要的那么准确。我的意思是,多个 return 语句不会禁止编译器 RVO(即使它确实使它无法实现),因此返回表达式仍然被视为 rvalue。
18赞 Zoner 7/10/2011 #4

简单的答案是,你应该像编写常规引用代码一样为右值引用编写代码,并且你应该在 99% 的时间里在心理上对它们一视同仁。这包括所有关于返回引用的旧规则(即从不返回对局部变量的引用)。

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

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

现在,右值引用变得有趣的地方是,您还可以将它们用作普通函数的参数。这允许您编写同时具有 const 引用 (const foo& other) 和右值引用 (foo&& other) 重载的容器。即使参数太笨拙而无法仅通过构造函数调用传递,它仍然可以完成:

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);
4赞 Red XIII 2/26/2013 #5

这不是一个答案本身,而是一个指导方针。大多数时候,声明局部变量没有多大意义(就像你对 所做的那样)。您仍然需要在类型方法中使用它们。还有一个已经提到的问题,当你尝试从函数中返回这样的函数时,你会得到标准的引用-被摧毁-临时-惨败。T&&std::vector<int>&& rval_refstd::move()foo(T&&)rval_ref

大多数时候,我会采用以下模式:

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

显然,在有些情况下(尽管相当罕见),函数真正返回 a,这是对非临时对象的引用,您可以将其移动到对象中。T&&

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

2赞 Andrej Podzimek 3/6/2019 #6

正如在对第一个答案的评论中已经提到的,除了返回局部变量之外,该构造还可以在其他情况下发挥作用。下面是一个可运行的示例,它记录了在返回有和没有成员对象时发生的情况: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 Cstruct A

请注意,即使对象是 R 值,也总是会被复制出。这是因为编译器无法判断 的实例不会再被使用。在 中,编译器确实有来自 的信息,这就是被移动的原因,除非 的实例是常量。struct Aclass Bclass Bclass Bstruct Aclass Cstd::move()struct Aclass C