首页 > 解决方案 > C++ 返回值和移动规则异常

问题描述

当我们从 C++ 函数返回一个值时,就会发生复制初始化。例如:

std::string hello() {
    std::string x = "Hello world";
    return x; // copy-init
}

假设 RVO 已禁用。

根据 copy-init 规则,如果x是非 POD 类类型,则应调用复制构造函数。但是对于 C++11 以后的版本,我看到调用了 move-constructor。我无法找到或理解有关此https://en.cppreference.com/w/cpp/language/copy_initialization的规则。所以我的第一个问题是——

  1. 当从函数返回值时,C++ 标准对 copy-init 发生的移动有什么看法?

  2. 作为上述问题的延伸,我还想知道在什么情况下不会发生移动。我想出了以下情况,其中调用了复制构造函数而不是移动:

std::string hello2(std::string& param) {
    return param;
}

最后,在一些库代码中,我看到std::move在返回时显式使用了它(即使 RVO 或 move 应该发生)。例如:

std::string hello3() {
    std::string x = "Hello world";
    return std::move(x);
}
  1. 返回时显式使用的优缺点是std::move什么?

标签: c++11initializationmovecopy-constructorrvo

解决方案


您对通过移动构造函数进行初始化是“复制初始化”的一种特殊情况这一事实感到困惑,并且不是单独的概念。检查 cppreference 页面上的注释。

如果 other 是右值表达式,则移动构造函数将由重载决议选择并在复制初始化期间调用。没有移动初始化这样的术语。

要从函数返回值,请查看cppreference 上的返回说明。它在一个名为“从局部变量和参数自动移动”的框中说,其中表达式是指您返回的内容(警告:该报价已缩短!阅读原文以获取有关其他情况的完整详细信息):

如果expression是一个(可能带括号的)id 表达式,它命名一个类型为 [...] 的变量为非易失性对象类型 [...] 并且该变量在正文中声明为 [...] 或作为[...] 函数的参数,然后执行两次重载决议以选择用于初始化返回值的构造函数:第一次好像表达式是一个右值表达式(因此它可以选择移动构造函数),如果第一次重载解析失败 [...] 然后像往常一样执行重载解析,表达式被视为左值(因此它可以选择复制构造函数)。

因此,在返回局部变量的特殊情况下,可以将变量视为 r 值,即使正常的语法规则会将其设为左值。该规则的精神是,返回后,在返回值的复制初始化过程中,您无法查明局部变量的值是否已被破坏,因此移动它不会造成任何损坏。

关于你的第二个问题:返回时使用被认为是不好的风格std::move,因为无论如何都会发生移动,并且它会抑制 NRVO

引用上面链接的 C++ 核心指南:

永远不要写return move(local_variable);,因为语言已经知道变量是移动候选者。编写move此代码无济于事,实际上可能是有害的,因为在某些编译器上,它会通过创建局部变量的附加引用别名来干扰 RVO(返回值优化)。

因此,您引用的库代码不是最理想的。

此外,您不能隐式移动非函数本地的任何内容(即局部变量和值参数),因为隐式移动可能会从函数返回后仍然可见的内容移动。在 cppreference 的引用中,重点是“非易失性对象类型”。当您返回时std::string& param,这是一个具有引用类型的变量。


推荐阅读