我什么时候应该将我的函数声明为:
void foo(Widget w);
与
void foo(Widget&& w);
?
假设这是唯一的重载(例如,我选择一个或另一个,而不是两者,也没有其他重载)。不涉及模板。假设函数 foo
需要 Widget
的所有权(例如 const Widget&
不是本讨论的一部分)。我对这些情况范围之外的任何答案都不感兴趣。请参阅帖子末尾的附录,了解 为什么 这些限制是问题的一部分。
我和我的同事能想出的主要区别是右值引用参数迫使你明确地说明副本。调用者负责制作显式副本,然后在需要副本时将其与 std::move
一起传递。在按值传递的情况下,复制的成本是隐藏的:
//If foo is a pass by value function, calling + making a copy:
Widget x{};
foo(x); //Implicit copy
//Not shown: continues to use x locally
//If foo is a pass by rvalue reference function, calling + making a copy:
Widget x{};
//foo(x); //This would be a compiler error
auto copy = x; //Explicit copy
foo(std::move(copy));
//Not shown: continues to use x locally
除了那个区别。除了强制人们明确复制和更改调用函数时获得的语法糖量之外,这些还有什么不同?他们对界面有什么不同的看法?它们之间的效率更高还是更低?
我和我的同事已经想到的其他事情:
- 右值引用参数意味着您 可以 移动参数,但不强制要求它。您在调用站点传递的参数可能会在之后处于其原始状态。也有可能该函数会吃掉/更改参数而不调用移动构造函数,但假设因为它是一个右值引用,调用者放弃了控制。按值传递,如果你进入它,你必须假设发生了移动;没有选择。
- 假设没有省略,通过右值传递消除了单个移动构造函数调用。
- 编译器有更好的机会通过值传递来省略副本/移动。任何人都可以证实这一说法吗?最好带有指向 gcc.godbolt.org 的链接,显示从 gcc/clang 生成的优化代码,而不是标准中的一行。我试图展示这一点可能无法成功隔离该行为: https ://godbolt.org/g/4yomtt
附录: 为什么 我这么限制这个问题?
- 没有重载 - 如果有其他重载,这将演变为讨论按值传递与包含 const 引用和 rvalue 引用的一组重载,此时重载集显然更有效并获胜。这是众所周知的,因此并不有趣。
- 没有模板 - 我对转发引用如何融入图片不感兴趣。如果您有转发引用,则无论如何都调用 std::forward 。转发参考的目标是在您收到东西时传递它们。副本不相关,因为您只需传递一个左值。这是众所周知的,并不有趣。
foo
需要拥有Widget
(又名const Widget&
) - 我们不是在谈论只读函数。如果该函数是只读的或者不需要拥有或延长Widget
的生命周期,那么答案就变成了const Widget&
,这又是众所周知的,而不是有趣的。我还向您介绍了为什么我们不想谈论重载。
原文由 Mark 发布,翻译遵循 CC BY-SA 4.0 许可协议
是的,通过右值引用传递得到了一点。
是的,按值传递是有道理的。
但这也为传递右值提供了处理异常保证的机会:如果
foo
抛出,则widget
不需要消耗值。对于仅移动类型(如
std::unique_ptr
),按值传递似乎是常态(主要用于您的第二点,而第一点无论如何都不适用)。编辑:标准库与我的前一句相矛盾,
shared_ptr
的构造函数采用std::unique_ptr<T, D>&&
。对于同时具有复制/移动的类型(如
std::shared_ptr
),我们可以选择与先前类型的一致性或强制在复制时显式。除非您想保证没有不需要的副本,否则我会使用按值传递来保持一致性。
除非您想要保证和/或立即接收,否则我会使用右值传递。
对于现有的代码库,我会保持一致性。