【问题标题】:Are returned locals automatically xvalues是否自动返回局部变量 xvalues
【发布时间】:2012-04-15 09:04:35
【问题描述】:

根据我对此发表的评论:

passing std::vector to constructor and move semantics 以下代码中是否需要std::move,以确保返回值是xvalue?

std::vector<string> buildVector()
{
  std::vector<string> local;

  // .... build a vector

  return std::move(local);
}

据我了解,这是必需的。我经常看到在从函数返回 std::unique_ptr 时使用它,但是 GManNickG 发表了以下评论:

据我了解,在 return 语句中,所有局部变量都会自动成为 xvalues(过期值)并将被移动,但我不确定这是否仅适用于返回的对象本身。所以OP应该继续把它放在那里,直到我更有信心它不应该是。 :)

谁能澄清std::move 是否有必要?

行为编译器是否依赖?

【问题讨论】:

  • 请注意,自从您修改我的声明以来,您已导致我修改。它只是被移动的返回值(可能是局部变量),而不是一般的所有局部变量。 (虽然这很好,但它可能会破坏一些我想不到的旧代码,并且 C++ 进程必须保持向后兼容性。)

标签: c++ c++11 move-semantics


【解决方案1】:

保证local 在这种情况下将作为右值返回。通常编译器会在这成为问题之前执行返回值优化,而且您可能根本看不到任何实际动作,因为 local 对象将直接在调用站点构造。

6.6.3 ["The return statement"] (2)中的一个相关

与 return 语句关联的复制或移动操作可能会被省略或视为右值,以便在选择构造函数 (12.8) 时进行重载决策。

为了澄清,这就是说返回的对象可以从本地对象移动构造(即使在实践中 RVO 将完全跳过这一步)。标准的规范部分是 12.8 [“复制和移动类对象”] (31, 32),关于复制省略和右值(感谢@Mankarse!)。


这是一个愚蠢的例子:

#include <utility>

struct Foo
{
    Foo()            = default;
    Foo(Foo const &) = delete;
    Foo(Foo &&)      = default;
};

Foo f(Foo & x)
{
    Foo y;

    // return x;         // error: use of deleted function ‘Foo::Foo(const Foo&)’
    return std::move(x); // OK
    return std::move(y); // OK
    return y;            // OK (!!)
}

将此与返回实际的右值引用进行对比:

Foo && g()
{
    Foo y;
    // return y;         // error: cannot bind ‘Foo’ lvalue to ‘Foo&&’
    return std::move(y); // OK type-wise (but undefined behaviour, thanks @GMNG)
}

【讨论】:

  • +1 (要清楚,最后一个有 UB,只应作为类型检查示例。)
  • @GManNickG:是的;除了forwardmove,你不会真正从任何地方返回&amp;&amp;。这是一个人为的例子。
  • 保证这种行为的规范段落是[class.copy]/32
【解决方案2】:

尽管return std::move(local)return local 都在编译,但它们的行为是不同的。并且可能只打算使用后者。

如果你编写了一个返回std::vector&lt;string&gt; 的函数,你必须返回一个std::vector&lt;string&gt; 并且完全正确。 std::move(local) 的类型为std::vector&lt;string&gt;&amp;&amp;,它不是 std::vector&lt;string&gt;,因此必须使用移动构造函数将其转换为它。

标准在 6.6.3.2 中说:

表达式的值是隐式的 转换为它出现的函数的返回类型。

也就是说,return std::move(local) 等价于

std::vector<std::string> converted(std::move(local); // move constructor
return converted; // not yet a copy constructor call (which will be elided anyway)

return local 只是

return local; // not yet a copy constructor call (which will be elided anyway)

这样可以省去一次操作。


给你一个简短的例子来说明这意味着什么:

struct test {
  test() { std::cout << "  construct\n"; }
  test(const test&) { std::cout << "  copy\n"; }
  test(test&&) { std::cout << "  move\n"; }
};

test f1() { test t; return t; }
test f2() { test t; return std::move(t); }

int main()
{
  std::cout << "f1():\n"; test t1 = f1();
  std::cout << "f2():\n"; test t2 = f2();
}

这将输出

f1():
  construct
f2():
  construct
  move

【讨论】:

  • 请注意,这仅在存在 NVRO/elision 的情况下才是正确的。如果没有省略,f1 也将不得不移动。
  • @NicolBolas:是的,这就是上面写的“还不是复制构造函数调用”部分(嗯,实际上它必须是移动构造函数调用)。但是,这会同时影响f1f2,所以即使这样f1 也少了一个构造函数调用。
  • @ipc: 否。在f2 中,最终返回值直接移动构造到输出中。所以它仍然只有 2 个构造函数:初始构造函数和移动构造函数。
【解决方案3】:

我认为答案是否定的。虽然官方只是一个注释,但第 5/6 节总结了哪些表达式是/不是 xvalues:

一个表达式是一个 xvalue 如果它是:

  • 调用函数的结果,无论是隐式还是显式,其返回类型是对对象类型的右值引用,
  • 对对象类型的右值引用的强制转换,
  • 一个类成员访问表达式,指定一个非引用类型的非静态数据成员,其中对象表达式是一个 xvalue,或
  • .* 指向成员的指针表达式,其中第一个操作数是 xvalue,第二个操作数是指向数据成员的指针。

一般来说,这条规则的效果是命名的右值引用被视为左值,而对对象的未命名的右值引用被视为xvalue;对函数的右值引用被视为左值,无论是否命名。

第一个要点似乎适用于此。由于所讨论的函数返回的是值而不是右值引用,因此结果不会是 xvalue。

【讨论】:

  • +1 用于谈论 xvalues,因为我也错了。 (我想不是我的日子。)幸运的是,真正重要的是返回值是否可以被视为右值,它就是。但并不是所有的 xvalues 都像我声称的那样。
猜你喜欢
  • 2011-09-15
  • 2022-06-23
  • 1970-01-01
  • 2020-06-22
  • 2021-11-07
  • 2014-05-14
  • 1970-01-01
  • 2010-10-02
  • 1970-01-01
相关资源
最近更新 更多