按值传递与按右值引用传递

新手上路,请多包涵

我什么时候应该将我的函数声明为:

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 许可协议

阅读 738
2 个回答

rvalue 引用参数强制您明确说明副本。

是的,通过右值引用传递得到了一点。

右值引用参数意味着您可以移动参数,但不强制要求它。

是的,按值传递是有道理的。

但这也为传递右值提供了处理异常保证的机会:如果 foo 抛出,则 widget 不需要消耗值。

对于仅移动类型(如 std::unique_ptr ),按值传递似乎是常态(主要用于您的第二点,而第一点无论如何都不适用)。

编辑:标准库与我的前一句相矛盾, shared_ptr 的构造函数采用 std::unique_ptr<T, D>&&

对于同时具有复制/移动的类型(如 std::shared_ptr ),我们可以选择与先前类型的一致性或强制在复制时显式。

除非您想保证没有不需要的副本,否则我会使用按值传递来保持一致性。

除非您想要保证和/或立即接收,否则我会使用右值传递。

对于现有的代码库,我会保持一致性。

原文由 Jarod42 发布,翻译遵循 CC BY-SA 4.0 许可协议

右值用法对接口与复制有何看法? rvalue 向调用者建议该函数既想拥有该值,又不打算让调用者知道它所做的任何更改。考虑以下内容(我知道您在示例中说没有左值引用,但请耐心等待):

 //Hello. I want my own local copy of your Widget that I will manipulate,
//but I don't want my changes to affect the one you have. I may or may not
//hold onto it for later, but that's none of your business.
void foo(Widget w);

//Hello. I want to take your Widget and play with it. It may be in a
//different state than when you gave it to me, but it'll still be yours
//when I'm finished. Trust me!
void foo(Widget& w);

//Hello. Can I see that Widget of yours? I don't want to mess with it;
//I just want to check something out on it. Read that one value from it,
//or observe what state it's in. I won't touch it and I won't keep it.
void foo(const Widget& w);

//Hello. Ooh, I like that Widget you have. You're not going to use it
//anymore, are you? Please just give it to me. Thank you! It's my
//responsibility now, so don't worry about it anymore, m'kay?
void foo(Widget&& w);

另一种看待它的方式:

 //Here, let me buy you a new car just like mine. I don't care if you wreck
//it or give it a new paint job; you have yours and I have mine.
void foo(Car c);

//Here are the keys to my car. I understand that it may come back...
//not quite the same... as I lent it to you, but I'm okay with that.
void foo(Car& c);

//Here are the keys to my car as long as you promise to not give it a
//paint job or anything like that
void foo(const Car& c);

//I don't need my car anymore, so I'm signing the title over to you now.
//Happy birthday!
void foo(Car&& c);

现在,如果小部件必须保持唯一性(就像 GTK 中的实际小部件一样),那么第一个选项就行不通了。第二、第三和第四个选项是有意义的,因为仍然只有一种数据的真实表示。无论如何,当我在代码中看到这些语义时,这就是我所说的。

现在,至于效率:这取决于。如果 Widget 有一个指向数据成员的指针,其指向的内容可能相当大(想想一个数组),则右值引用可以节省大量时间。由于调用者使用了右值,他们说他们不再关心他们给你的东西了。因此,如果您想将调用者的 Widget 的内容移动到您的 Widget 中,只需获取他们的指针即可。无需仔细复制指针指向的数据结构中的每个元素。这可以很好地提高速度(再次,想想数组)。但是如果 Widget 类没有任何这样的东西,那么这种好处是无处可寻的。

希望这能满足您的要求;如果没有,我也许可以扩展/澄清事情。

原文由 Altainia 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题