通过价值传递与通过右值参考

时间:2016-06-21 04:05:49

标签: c++ c++11 c++14

我什么时候应该将我的功能声明为:

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

除了这种差异。除了强迫人们明确复制和改变调用函数时获得的语法糖量,这些还有什么不同?他们对界面有何不同看法?它们的效率是否高于或低于其他人?

我和我的同事已经想过的其他事情:

  • rvalue引用参数表示您可能移动参数,但不强制它。您在呼叫站点传递的参数可能会在之后处于其原始状态。它也可能在不调用移动构造函数的情况下使用/更改参数,但假设因为它是一个右值引用,调用者放弃了控制。通过价值,如果你进入它,你必须假设一个移动发生;没有选择。
  • 假设没有elisions,则通过rvalue消除单个移动构造函数调用。
  • 编译器有更好的机会以传递值来消除副本/移动。有人可以证实这种说法吗?最好有一个链接到gcc.godbolt.org,显示来自gcc / clang的优化生成代码,而不是标准中的一行。我尝试显示此信息可能无法成功隔离行为:https://godbolt.org/g/4yomtt

附录: 为什么我这么多地限制了这个问题?

  • 没有重载 - 如果有其他重载,这将转变为关于传递值与包含const引用和右值引用的一组重载的讨论,此时重载集显然更有效并获胜。这是众所周知的,因此并不有趣。
  • 没有模板 - 我对转发引用如何适应图片不感兴趣。如果您有转发引用,则无论如何都要调用std :: forward。转发引用的目标是在收到它们时传递内容。副本并不相关,因为您只需传递左值。这是众所周知的,而且并不有趣。
  • foo需要Widget的所有权(又名const Widget&) - 我们不是在谈论只读功能。如果函数是只读的,或者不需要拥有或延长Widget的生命周期,则答案通常会变为const Widget&,这也是众所周知的,而且并不有趣。我还提到你为什么我们不想谈论过载。

7 个答案:

答案 0 :(得分:9)

除非类型是一个只移动类型,否则你通常可以选择通过引用传递给const,而且它似乎是任意的,使它“不是讨论的一部分”,但我会尝试。

我认为选择部分取决于foo对参数的影响。

该功能需要本地副本

假设Widget是一个迭代器,您想要实现自己的std::next函数。 next需要自己的副本才能前进然后返回。在这种情况下,您的选择类似于:

Widget next(Widget it, int n = 1){
    std::advance(it, n);
    return it;
}

VS

Widget next(Widget&& it, int n = 1){
    std::advance(it, n);
    return std::move(it);
}

我认为按价值在这里更好。从签名中你可以看到它正在复制。如果调用者想要避免复制,他们可以执行std::move并保证变量被移动但是如果他们愿意,他们仍然可以传递左值。 通过rvalue-reference传递,调用者无法保证变量已被移动。

移动分配到副本

假设你有一个班级WidgetHolder

class WidgetHolder {
    Widget widget;
   //...
};

您需要实现setWidget成员函数。我假设你已经有一个带引用const的重载:

WidgetHolder::setWidget(const Widget& w) {
    widget = w;
}

但在测量性能后,您决定需要针对r值进行优化。您可以选择将其替换为:

WidgetHolder::setWidget(Widget w) {
    widget = std::move(w);
}

或超载:

WidgetHolder::setWidget(Widget&& widget) {
    widget = std::move(w);
}

这个有点棘手。它很诱人选择pass-by-value,因为它接受rvalues和lvalues,所以你不需要两个重载。但是,它无条件地复制,因此您无法利用成员变量中的任何现有容量。通过引用传递给const并传递r值引用重载使用赋值而不需要复制可能更快的副本

移动构建副本

现在假设你正在编写WidgetHolder的构造函数,并且像以前一样已经实现了一个带有引用的构造函数:

WidgetHolder::WidgetHolder(const Widget& w) : widget(w) {
}

和以前一样,你已经测量了性能,并决定你需要优化rvalues。您可以选择将其替换为:

WidgetHolder::WidgetHolder(Widget w) : widget(std::move(w)) {
}

或超载:

WidgetHolder::WidgetHolder(Widget&& w) : widget(std:move(w)) {
}

在这种情况下,成员变量不能具有任何现有容量,因为这是构造函数。你是移动构建副本。此外,构造函数通常采用许多参数,因此编写所有不同的重载排列以优化r值引用可能会非常痛苦。所以在这种情况下,使用pass-by-value是个好主意,特别是如果构造函数需要很多这样的参数。

传递unique_ptr

unique_ptr而言,效率问题并不那么重要,因为此举很便宜并且没有任何容量。更重要的是表现力和正确性。对如何通过unique_ptr here进行了很好的讨论。

答案 1 :(得分:9)

关于界面与复制的rvalue用法有什么用? 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);

现在,如果Widgets必须保持唯一(例如,GTK中的实际小部件),那么第一个选项无效。第二,第三和第四选项是有意义的,因为仍然只有一个真实的数据表示。无论如何,当我在代码中看到它们时,这就是那些语义对我说的。

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

希望得到你所要求的;如果没有,我可以扩展/澄清一些事情。

答案 2 :(得分:8)

  

右值参考参数强制您明确复制副本。

是的,传递右值 - 参考得分。

  

rvalue引用参数表示您可以移动参数,但不强制它。

是的,按值传递得到了一分。 但这也为r-by-by-rvalue提供了处理异常保证的机会: 如果foo抛出,则无需消耗widget值。

对于仅移动类型(如std::unique_ptr),传值似乎是常态(主要是第二点,第一点不适用)。

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

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

除非你想要保修,否则没有不需要的副本,我会使用按值传递来保持一致性。 除非你想要保证和/或立即接收,否则我会使用右移传递。

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

答案 3 :(得分:4)

当您通过右值参考对象时,生命周期变得复杂。如果被调用者没有移出参数,则参数的销毁将被延迟。我认为这有两个案例很有意思。

首先,你有一个RAII类

void fn(RAII &&);

RAII x{underlying_resource};
fn(std::move(x));
// later in the code
RAII y{underlying_resource};

初始化y时,如果x移出rvalue引用,则资源仍可由fn保留。在传递价值代码中,我们知道x已移出,fn发布x。这可能是您希望按值传递的情况,并且可能会删除复制构造函数,因此您不必担心意外副本。

其次,如果参数是一个大对象且函数没有移出,则向量数据的生命周期大于传递值的情况。

vector<B> fn1(vector<A> &&x);
vector<C> fn2(vector<B> &&x);

vector<A> va;  // large vector
vector<B> vb = fn1(std::move(va));
vector<C> vc = fn2(std::move(vb));

在上面的示例中,如果fn1fn2移出x,那么您将最终获得所有向量中的所有数据活。如果您改为按值传递,则只有最后一个向量的数据仍然有效(假设向量移动构造函数清除了源向量)。

答案 4 :(得分:3)

其他答案中没有提到的一个问题是异常安全的概念。

一般情况下,如果函数抛出异常,我们理想情况下要使用强异常保证,这意味着除了引发异常之外,调用没有任何效果。如果按值传递使用移动构造函数,那么这种效果基本上是不可避免的。因此,在某些情况下,rvalue-reference参数可能更优越。 (当然,在各种情况下,强大的例外保证无法以任何方式实现,以及无论如何都可以获得无投保证的各种情况。因此,这与100%的情况无关。但是它有时是相关的。)

答案 5 :(得分:2)

在by-value和by-rvalue-ref之间选择,没有其他重载,没有意义。

使用pass by value,实际参数可以是左值表达式。

通过rvalue-ref传递,实际参数必须是右值。

如果函数存储了参数的副本,那么一个明智的选择是在pass-by-value和一组带有pass-by-ref-to-const和pass-by-rvalue-ref的重载之间。对于作为实际参数的rvalue表达式,重载集可以避免一次移动。微观优化是否值得增加复杂性和打字是一种工程直觉决定。

答案 6 :(得分:1)

一个显着的区别是,如果您转到值传递函数:

void foo(Widget w);
foo(std::move(copy));

编译器必须生成一个移动构造函数调用Widget(Widget&&)来创建值对象。在通过右值引用传递的情况下,不需要进行任何调用,因为右值引用直接传递给方法。通常这无关紧要,因为move构造函数是微不足道的(或默认设置),并且在大多数情况下都内联。 (您可以在gcc.godbolt.org上进行检查-在您的示例中声明move构造函数Widget(Widget&&);,它将显示在程序集中)

所以我的经验法则是这样:

  • 如果对象表示唯一资源(没有复制语义),我更喜欢使用pass-by-rvalue-reference
  • 否则,如果在逻辑上移动或复制对象有意义,那么我将使用传递值。