正确使用右值引用作为参数
以下面的方法为例:
void Asset::Load( const std::string& Path ) { // complicated method.... }
这种方法的一般用法如下:
Asset ExampleAsset; ExampleAsset.Load("image0.png");
由于我们大多数时候都知道Path是一个临时右值,因此添加此方法的Rvalue版本是否有意义? 如果是这样,这是一个正确的实施;
void Asset::Load( const std::string& Path ) { // complicated method.... } void Asset::Load( std::string&& Path ) { Load(Path); // call the above method }
这是一个正确的方法来编写右值版本的方法吗?
对于你的具体情况,第二个超载是没用的。
用原来的代码,只有一个负载的重载,这个函数被称为左值和右值。
使用新的代码,第一个重载被称为左值,第二个被称为右值。 但是,第二个重载称为第一个重载。 最后,调用一个或另一个的效果意味着将执行相同的操作(不pipe第一个重载是什么)。
因此,原始代码和新代码的效果是相同的,但第一个代码更简单。
决定一个函数是否必须通过值来引用参数,左值引用或右值引用很大程度上取决于它的作用。 当你想移动传递的参数时,你应该提供一个重载的右值引用。 移动semantincs有几个很好的参考 ,所以我不会在这里覆盖。
奖励 :
为了帮助我使我的观点考虑这个简单的probe
类:
struct probe { probe(const char* ) { std::cout << "ctr " << std::endl; } probe(const probe& ) { std::cout << "copy" << std::endl; } probe(probe&& ) { std::cout << "move" << std::endl; } };
现在考虑这个function:
void f(const probe& p) { probe q(p); // use q; }
调用f("foo");
产生以下输出:
ctr copy
这里没有什么惊喜:我们创build了一个传递const char*
"foo"
的临时probe
。 因此,第一条输出线。 然后,这个临时的被绑定到p
并且在f
创build了一个p
的副本。 因此第二条输出线。
现在,考虑按价值取值,即将f
改为:
void f(probe p) { // use p; }
f("foo");
的输出f("foo");
就是现在
ctr
在这种情况下,有些人会感到惊讶:没有副本! 一般来说,如果您通过引用来引用参数并将其复制到您的函数中,那么最好是通过值来引用参数。 在这种情况下,编译器可以直接从input( "foo"
)构造参数(在这种情况下为p
),而不是创build临时文件并将其复制。 欲了解更多信息,请参阅想要速度? 通过价值。 由戴夫Abrahams。
这个准则有两个显着的例外:构造函数和赋值操作符。
考虑这个类:
struct foo { probe p; foo(const probe& q) : p(q) { } };
构造函数通过const引用进行probe
,然后将其复制到p
。 在这种情况下,遵循上面的指导原则并不会带来任何性能上的改进,而probe
的拷贝构造函数将会被调用。 但是,按价值计算可能会产生一个类似于现在我将要讲述的赋值运算符的重载解决问题。
假设我们的类probe
有一个非抛出的swap
方法。 然后,它的赋值运算符的build议实现(暂时考虑C ++ 03术语)是
probe& operator =(const probe& other) { probe tmp(other); swap(tmp); return *this; }
那么,根据上面的指导,最好是这样写
probe& operator =(probe tmp) { swap(tmp); return *this; }
现在用rvalue引用进入C ++ 11并移动语义。 你决定添加一个移动赋值操作符:
probe& operator =(probe&&);
现在在临时调用赋值运算符时会产生歧义,因为两个重载都是可行的,没有一个比另一个更受欢迎。 要解决此问题,请使用赋值运算符的原始实现(通过const引用获取参数)。
实际上,这个问题并不是特定于构造函数和赋值操作符,而可能发生在任何函数上。 (更有可能的是,你将通过构造函数和赋值操作符来体验它。)例如,调用g("foo");
当g
有以下两个重载时引起模糊:
void g(probe); void g(probe&&);
除非要调用Load
的左值引用版本,否则不需要第二个函数,因为右值将绑定到const左值引用。
这通常是一个问题,你是否会在内部复制(显式地或隐式地)传入的对象(提供T&&
参数),或者你只是使用它(stick to [const] T&
)。
如果你的Load
成员函数没有从传入的string中分配,你应该简单地提供void Asset::Load(const std::string& Path)
。
如果你是从传入path
分配的,那么说一个成员variables,那么也有一种情况,提供void Asset::Load(std::string&& Path)
也可以更有效率,但是你需要一个不同的实现分配ala loaded_from_path_ = std::move(Path);
。
调用者的潜在好处在于,使用&&
版本,他们可能会收到由成员variables拥有的自由存储区域,避免在void Asset::Load(const std::string& Path)
并在下一次调用者的string被分配时重新分配(假设缓冲区足够大以适应其下一个值)。
在你说的情况下,你通常会传递string文字; 这样的调用者将不会从任何&&
重载中受益,因为没有调用者拥有的std::string
实例来接收现有数据成员的缓冲区。
由于我们大多数时候都知道Path是一个临时右值,因此添加此方法的Rvalue版本是否有意义?
可能不会…除非你需要在Load()
中做一些棘手的事情,这需要一个非const的参数。 例如,也许你想std::move(Path)
到另一个线程。 在这种情况下,使用移动语义可能是有意义的。
这是一个正确的方法来编写右值版本的方法吗?
不,你应该这样做:
void Asset::load( const std::string& path ) { auto path_copy = path; load(std::move(path_copy)); // call the below method } void Asset::load( std::string&& path ) { // complicated method.... }