我什么时候应该声明我的功能:
void foo(Widget w);
而不是
void foo(Widget&& w);?
假设这是唯一的重载(例如,我选择一个或另一个,不是两个,也没有其他重载).没有涉及模板.假设该函数foo需要所有权Widget(例如const Widget&,不是本讨论的一部分).我对这些情况范围之外的任何答案都不感兴趣.请参阅帖子末尾的附录,了解为什么这些约束是问题的一部分.
我和我的同事可以提出的主要区别是rvalue参考参数强制您明确复制副本.调用者负责制作一个显式副本,然后在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
Run Code Online (Sandbox Code Playgroud)
除了那个差异.除了强迫人们明确复制和改变调用函数时获得的语法糖量,这些还有什么不同?他们对界面有何不同看法?它们的效率是否高于或低于其他?
我和我的同事已经想过的其他事情:
附录: 为什么我如此限制这个问题?
foo需要所有权Widget(又名否const Widget&) - 我们不是在谈论只读函数.如果函数是只读的或者不需要拥有或延长它的生命周期Widget,那么答案就会变得简单const Widget&,这也是众所周知的,而且并不有趣.我还提到你为什么我们不想谈论重载.Alt*_*nia 14
关于界面与复制,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);
Run Code Online (Sandbox Code Playgroud)
另一种看待它的方式:
//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);
Run Code Online (Sandbox Code Playgroud)
现在,如果Widgets必须保持唯一(例如,GTK中的实际小部件),那么第一个选项不起作用.第二,第三和第四选项是有意义的,因为仍然只有一个真实的数据表示.无论如何,当我在代码中看到它们时,这就是那些语义对我说的.
现在,至于效率:它取决于.如果Widget具有指向数据成员的指针,则rvalue引用可以节省大量时间,该数据成员的指向内容可能相当大(想想一个数组).由于呼叫者使用右值,他们说他们不关心他们给你的东西了.因此,如果您想将调用者的Widget内容移动到您的Widget中,只需使用他们的指针即可.无需精心复制其指针指向的数据结构中的每个元素.这可以带来相当好的速度提升(再次,思考数组).但是如果Widget类没有任何这样的东西,那么这个好处就无处可见了.
希望能得到你所要求的; 如果没有,我可以扩展/澄清一些事情.
Chr*_*rew 10
除非类型是一个只移动类型,你通常有一个选项来传递引用到const,似乎任意使它"不是讨论的一部分",但我会尝试.
我认为选择部分取决于foo参数的作用.
假设Widget是一个迭代器,你想实现自己的std::next函数.next需要自己的副本才能提前然后返回.在这种情况下,您的选择是这样的:
Widget next(Widget it, int n = 1){
std::advance(it, n);
return it;
}
Run Code Online (Sandbox Code Playgroud)
VS
Widget next(Widget&& it, int n = 1){
std::advance(it, n);
return std::move(it);
}
Run Code Online (Sandbox Code Playgroud)
我认为按价值在这里更好.从签名中你可以看到它正在复制.如果调用者想要避免副本,他们可以做一个std::move并保证变量被移出但是如果他们愿意,他们仍然可以传递左值.通过rvalue-reference传递,调用者无法保证变量已被移动.
假设你有一个班级WidgetHolder:
class WidgetHolder {
Widget widget;
//...
};
Run Code Online (Sandbox Code Playgroud)
你需要实现一个setWidget成员函数.我假设你已经有一个带引用const的重载:
WidgetHolder::setWidget(const Widget& w) {
widget = w;
}
Run Code Online (Sandbox Code Playgroud)
但在测量性能后,您决定需要针对r值进行优化.您可以选择将其替换为:
WidgetHolder::setWidget(Widget w) {
widget = std::move(w);
}
Run Code Online (Sandbox Code Playgroud)
或者超载:
WidgetHolder::setWidget(Widget&& widget) {
widget = std::move(w);
}
Run Code Online (Sandbox Code Playgroud)
这个有点棘手.它很诱人选择pass-by-value,因为它接受rvalues和lvalues,所以你不需要两个重载.但是,它无条件地复制,因此您无法利用成员变量中的任何现有容量.通过引用传递给const并传递r值引用重载使用赋值而不需要复制可能更快的副本
现在假设你正在编写构造函数,WidgetHolder并且像以前一样已经实现了一个带有引用的构造函数:
WidgetHolder::WidgetHolder(const Widget& w) : widget(w) {
}
Run Code Online (Sandbox Code Playgroud)
和之前一样,你已经测量了性能,并决定你需要优化rvalues.您可以选择将其替换为:
WidgetHolder::WidgetHolder(Widget w) : widget(std::move(w)) {
}
Run Code Online (Sandbox Code Playgroud)
或者超载:
WidgetHolder::WidgetHolder(Widget&& w) : widget(std:move(w)) {
}
Run Code Online (Sandbox Code Playgroud)
在这种情况下,成员变量不能具有任何现有容量,因为这是构造函数.你正在移动构建一个副本.此外,构造函数通常采用许多参数,因此编写所有不同的重载排列以优化r值引用可能会非常痛苦.所以在这种情况下,使用pass-by-value是个好主意,特别是如果构造函数需要很多这样的参数.
unique_ptr随着unique_ptr效率的担忧不太重要,因为一个举动是如此便宜,它没有任何能力.更重要的是表现力和正确性.关于如何通过unique_ptr 这里有一个很好的讨论.
右值参考参数强制您明确表示副本.
是的,通过右值参考获得了一个观点.
rvalue引用参数意味着您可以移动参数,但不强制它.
是的,按值传递得到了一分.但是,这也为r-pass-by-rvalue提供了处理异常保证的机会:如果foo抛出,widget则不需要消耗价值.
对于仅移动类型(as std::unique_ptr),pass-by-value似乎是常态(主要针对您的第二点,并且第一点不适用).
编辑:标准库与我之前的句子相矛盾,其中一个shared_ptr构造函数需要std::unique_ptr<T, D>&&.
对于同时具有复制/移动(as std::shared_ptr)的类型,我们可以选择与先前类型的一致性或强制在复制时使用.
除非你想保证没有不需要的副本,否则我会使用按值传递来实现一致性.
除非你想要保证和/或立即接收,否则我会使用右移传递.
对于现有的代码库,我会保持一致性.
其他答案中没有提到的一个问题是异常安全的想法。
一般来说,如果函数抛出异常,我们理想情况下希望有强异常保证,这意味着调用除了引发异常之外没有任何影响。如果传值使用了移动构造函数,那么这种效果本质上是不可避免的。因此,在某些情况下,右值引用参数可能更好。(当然,在各种情况下,无论哪种方式都无法实现强异常保证,以及各种情况下都可以使用不抛出保证。所以这在 100% 的情况下并不相关。但它是相关的有时。)
当您通过右值引用对象生命周期变得复杂。如果被调用者没有移出参数,则延迟销毁参数。我认为这在两种情况下都很有趣。
首先,你有一个 RAII 类
void fn(RAII &&);
RAII x{underlying_resource};
fn(std::move(x));
// later in the code
RAII y{underlying_resource};
Run Code Online (Sandbox Code Playgroud)
初始化时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));
Run Code Online (Sandbox Code Playgroud)
在上面的示例中,如果fn1并且fn2不移出x,那么您最终将得到所有向量中的所有数据仍然存在。如果改为按值传递,则只有最后一个向量的数据仍然有效(假设向量移动构造函数清除了源向量)。