按值传递vs按右值引用传递


72

我何时应该将函数声明为:

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

除了那个区别。除了强迫人们明确地复制和更改调用函数时获得多少语法糖外,这些还有什么不同?他们对界面有何不同看法?它们是彼此相比效率更高还是更低?

我和我的同事已经想到的其他事项:

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

附录: 为什么我要限制这么多的问题?

  • 没有重载-如果还有其他重载,则将讨论按值传递与一组重载(包括const引用和rvalue引用)的重载的讨论,此时重载的集合显然更有效并且更成功。这是众所周知的,因此并不有趣。
  • 没有模板-我对转发引用如何适合图片不感兴趣。如果您有转发参考,则无论如何都要调用std :: forward。具有转发参考的目标是在您收到邮件时传递它们。副本无关紧要,因为您只是传递了一个左值。这是众所周知的,并不有趣。
  • foo需要拥有Widget(aka no const Widget&)的所有权-我们不是在谈论只读功能。如果该函数是只读的,或者不需要拥有或延长的寿命Widget,那么答案就变成了const Widget&,这又是众所周知的,而且没有意思。我也向您介绍为什么我们不想谈论重载。

为什么不使用std::move而不是制作中间副本呢?
CinchBlue '16

@VermillionAzure-如果我以后不打算使用该变量,则可以这样做。关键是,如果我确实需要一份副本,那么现在是明确的。该示例假定出于某种原因需要复制。
2016年

这取决于foo参数的作用。像这样的非成员函数需要获得参数所有权是很不寻常的。
克里斯·德鲁

2
这两个接口不可互换,因为传递值也采用l值。因此,我不确定在不指定使用限制的情况下是否可以进行有意义的比较。
MikeMB

1
这个问题很广泛。无论是否要修改对象,如果您对其进行约束,以使该功能始终修改要获取更多主题答案的对象,那将有很大的不同。
MM

Answers:


12

rvalue参考参数强制您对副本明确。

是的,通过右值引用很重要。

右值引用参数表示您可以移动参数,但不强制使用该参数。

是的,按值传递很有意义。

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

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

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

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

除非您想保证没有多余的副本,否则我将使用值传递来保持一致性。

除非您想要保证和/或立即下沉,否则我将使用传递右值。

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


4
rvalue-ref传递不表示与值传递相同的接口。调用代码不能做同样的事情。因此,当您根据类型推荐一个而不是另一个时,这没有意义。
干杯和健康。-Alf

@ Cheersandhth.-Alf:在问题中,都是明示所有权转移的,一个允许隐式复制。
Jarod42 '17

@ Jarod42:很明显,我当时回复的内容已在清理中删除。现在,我对自己的论点感到困惑。我仍然认为最好的选择是传递右值引用,但是我认为这是最佳选择,因为它可以最清楚地将所有权的获取传达给代码的读者。我看不出我在回答中写了任何有关源代码的内容作为通信。嗯!
干杯和健康。-Alf

37

关于接口与复制的右值用法怎么说?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类没有任何此类东西,那么这种好处是无处可寻的。

希望能达到您的要求;如果没有,我也许可以扩大/澄清。


9
这个答案错过了有人将小部件或汽车移入第一种情况的可能性,从而仍然避免了复制。这个void foo(Car c)案例并不是说我们每个人都有车,而是说“我需要拥有一辆车”,它可能是新车(副本),可能是您已经决定不再需要自己的车了,把它给我。那时,案例1和案例4之间的区别在于副本的明确性(就接口而言)。然后,直到那时,性能问题才变得有趣。其他情况仍超出范围。
2016年

3
@Mark你是正确的;void foo(Car c)最终,将汽车移入会产生相同的结果;这只是做作的一种人为方式。简想要一辆汽车,鲍勃不再需要他了。但是珍妮不想强加给鲍勃,所以她拒绝乘坐鲍勃的私家车。只想要一个喜欢它的人。鲍勃想摆脱他的汽车,然后假装给她换了一辆新车,但确实给了他他的。该用例是两个人并没有真正同意接口应该是什么。此外,我还提到了其他完整案例:)
Altainia

这是我所读过的关于按值传递,左值ref,常量左值ref和右值ref的最佳解释之一。真!
拉波

12

除非该类型是仅移动类型,否则您通常可以选择按引用传递const,并且使其成为“不是讨论的一部分”似乎是任意的,但我会尝试。

我认为选择部分取决于foo参数的作用。

该功能需要本地副本

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

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

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

我认为按值计算在这里更好。从签名中您可以看到它正在复印。如果呼叫者想要避免复制,则可以执行std::move并确保将变量从中移出,但是如果愿意,他们仍然可以传递左值。使用传递右值引用,调用者无法保证已将变量移出。

移动分配到副本

假设您有一堂课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);
}

这一点比较棘手。吸引人的是选择按值传递,因为它同时接受rvalue和lvalues,因此您不需要两个重载。但是,它是无条件复制的,因此您无法利用成员变量中的任何现有容量。按引用传递给const和按r值传递引用重载使用赋值而不获取副本,这可能会更快

移动构造副本

现在,假设您正在编写的构造函数WidgetHolder,就像以前一样,已经实现了采用对const的引用的构造函数:

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

和之前一样,您要测量性能并决定需要针对右值进行优化。您可以选择替换为:

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

或超载:

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

在这种情况下,成员变量不能具有任何现有容量,因为这是构造函数。您正在移动构造一个副本。而且,构造函数通常会使用许多参数,因此编写所有不同的重载排列以优化r值引用可能会很痛苦。因此,在这种情况下,最好使用按值传递,尤其是在构造函数采用许多此类参数的情况下。

通过 unique_ptr

鉴于unique_ptr效率如此低廉,而且搬迁没有任何能力,因此对效率的关注并不那么重要。更重要的是表达力和正确性。关于如何通过unique_ptr 这里有很好的讨论。


您的next的右值参数版本执行的工作超出其所需。它的界面显示“我拥有您给我的迭代器”;无需在函数内部进行复制。当且仅当必要时,这才在呼叫站点发生。因此,关于下一个的讨论是没有意义的。剩下的讨论完全是关于重载的,而const&是重载之一。在这种情况下,众所周知rvalue参数会更好。对那些情况不感兴趣。
2016年

更正我的最后一条评论:无需在函数内进行复制移动。除非另有说明,否则呼叫者必须假定它已被移动。还有代码行!=汇编行。
2016年

@Mark你是对的,我不需要显式地制作副本,next尽管它只是将副本推迟到返回之前。关于“ ...众所周知,右值参数更好”,我尝试提供一个示例(构造函数),其中按值更好。
克里斯·德鲁

@Mark,如果您选择按值接受参数的构造函数,则不再需要按引用传递给const构造函数
克里斯·德鲁

1
@dragonxlwang注意,这不是一招构造函数WidgetHolder这将是:WidgetHolder::WidgetHolder(WidgetHolder&&)。这是一个通常采用的普通构造函数Widget。在这种情况下,传递r值引用没有什么特别的错误,但是您必须具有两个重载WidgetHolder::WidgetHolder(Widget&& w)WidgetHolder::WidgetHolder(const Widget& w)而不是一个没有任何特殊好处的重载。如果您的构造函数带有许多参数,则这特别糟糕,因为这样您就需要许多重载的排列。
克里斯·德鲁

5

当您通过右值引用对象时,其生存期会变得很复杂。如果被调用方没有移出参数,则参数的销毁将延迟。我认为这在两种情况下都很有趣。

首先,您有一个RAII课程

void fn(RAII &&);

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

初始化时y,如果资源没有移出右值引用,x则仍可以保留该资源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,则最终所有矢量中的所有数据仍然有效。如果改为按值传递,则仅最后一个向量的数据仍然有效(假设向量move构造函数清除了源向量)。


这是一个坚实的,有充分理由的区别,其他人都没有涉及。这是微妙而令人敬畏的。可以解决template<typename T> typename std::remove_reference<T>::type move_fo_realzies(T&& x) { return std::move(x); }。该问题仅在您使用...而std::move不是move_fo_realzies...时才起作用,但是要让人们使用后者将需要进行一些其他的再教育。
2016年

5

其他答案中未提及的一个问题是异常安全性。

通常,如果函数引发异常,则理想情况下,我们希望具有较强的异常保证,这意味着调用除了引发异常外没有其他作用。如果按值传递使用move构造函数,那么这种效果本质上是不可避免的。因此,在某些情况下,右值引用参数可能更好。(当然,在很多情况下都无法实现强异常保证,以及在两种情况下都不能提供无掷保证的情况。因此,这在100%的情况下都无关紧要。但这是相关的有时。)


2

在按值和按值引用之间进行选择(没有其他重载)没有意义。

通过值传递,实际参数可以是左值表达式。

使用rvalue-ref传递时,实际参数必须为rvalue。


如果函数存储的是参数的副本,那么明智的选择是在按值传递和一组将常量传递给const和传递右值引用的重载之间。对于右值表达式作为实际参数,一组重载可以避免一个步骤。微观优化是否值得增加复杂性和类型决定,这是一个工程上的直觉。


2
这个答案避免了这个问题。除了第一句话外,答案在事实上是正确的,但没有关联。我拒绝根据OP中列举的差异列出第一句话。
2016年

@Mark:如果我知道阻止你摆脱这种愚蠢的方法,我不会。
干杯和健康。-Alf

2
需要明确的是,问题不在于在给定的有限情况下在两个列举的替代方案之间的明智选择。这将是一个愚蠢的问题。问题实际上是关于两个枚举的备选方案之间的技术差异和代码与人之间的通信差异,并提供了有限的环境,以隔离正在考虑的案件并将案件简化为最小形式。
yfeldblum

1

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

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

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

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

  • 如果对象表示唯一资源(没有复制语义),则我更喜欢使用pass-by-rvalue-reference,
  • 否则,如果从逻辑上讲移动或复制对象都有意义,则使用值传递。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.