我什么时候应该将我的功能声明为:
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
除了这种差异。除了强迫人们明确复制和改变调用函数时获得的语法糖量,这些还有什么不同?他们对界面有何不同看法?它们的效率是否高于或低于其他人?
我和我的同事已经想过的其他事情:
附录: 为什么我这么多地限制了这个问题?
foo
需要Widget
的所有权(又名const Widget&
) - 我们不是在谈论只读功能。如果函数是只读的,或者不需要拥有或延长Widget
的生命周期,则答案通常会变为const Widget&
,这也是众所周知的,而且并不有趣。我还提到你为什么我们不想谈论过载。答案 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));
在上面的示例中,如果fn1
和fn2
移出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&&);
,它将显示在程序集中)
所以我的经验法则是这样: