我们以下面的方法为例:
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
}
这是编写rvalue版本方法的正确方法吗?
答案 0 :(得分:60)
对于您的特定情况,第二次重载是无用的。
对于Load
只有一个重载的原始代码,将为左值和右值调用此函数。
使用新代码,第一个重载调用lvalues,第二个重载调用rvalues。但是,第二个重载调用第一个重载。最后,调用一个或另一个的效果意味着将执行相同的操作(无论第一次重载是什么)。
因此,原始代码和新代码的效果相同,但第一个代码更简单。
确定函数是否必须按值,左值引用或右值引用取一个参数,在很大程度上取决于它的作用。当你想移动传递的参数时,你应该提供一个带右值引用的重载。移动semantincs有几个好references,所以我不会在这里介绍它。
<强>加成强>:
为了帮助我,请考虑这个简单的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; }
};
现在考虑这个功能:
void f(const probe& p) {
probe q(p);
// use q;
}
调用f("foo");
会产生以下输出:
ctr
copy
此处不出意外:我们通过probe
const char*
创建了一个临时的"foo"
。因此第一条输出线。然后,此临时文件绑定到p
,并在q
内创建p
的{{1}}副本。因此第二个输出线。
现在,请考虑按值f
,即将p
更改为:
f
void f(probe p) {
// use p;
}
的输出现在是
f("foo");
在这种情况下,有些人会感到惊讶:没有副本!通常,如果您通过引用获取参数并将其复制到函数内部,那么最好按值获取参数。在这种情况下,编译器可以直接从输入(ctr
)构造参数(在本例中为p
),而不是创建临时和复制它。有关更多信息,请参阅Dave Abrahams的Want Speed? Pass by Value.。
本指南有两个值得注意的例外:构造函数和赋值运算符。
考虑这个课程:
"foo"
构造函数通过const引用获取struct foo {
probe p;
foo(const probe& q) : p(q) { }
};
,然后将其复制到probe
。在这种情况下,遵循上面的准则并没有带来任何性能提升,无论如何都会调用p
的拷贝构造函数。但是,按值probe
可能会产生一个重载决策问题,类似于我现在要介绍的赋值运算符。
假设我们的班级q
有一个非投掷probe
方法。然后建议的赋值运算符的实现(暂时用C ++ 03术语思考)是
swap
然后,根据上面的指导原则,最好像这样写
probe& operator =(const probe& other) {
probe tmp(other);
swap(tmp);
return *this;
}
现在输入带有右值引用的C ++ 11并移动语义。您决定添加移动赋值运算符:
probe& operator =(probe tmp) {
swap(tmp);
return *this;
}
现在在临时调用赋值运算符会产生歧义,因为两个重载都是可行的,并且没有一个优先于另一个。要解决此问题,请使用赋值运算符的原始实现(通过const引用获取参数)。
实际上,此问题并非特定于构造函数和赋值运算符,并且可能会出现在任何函数中。 (尽管如此,你很可能会遇到构造函数和赋值运算符。)例如,当probe& operator =(probe&&);
具有以下两个重载时调用g("foo");
会引起歧义:
g
答案 1 :(得分:7)
除非您正在执行除Load
的左值引用版本之外的其他操作,否则您不需要第二个函数,因为rvalue将绑定到const左值引用。
答案 2 :(得分:4)
由于我们大多数时候都知道Path是一个临时右值,所以添加这个方法的Rvalue版本是否有意义?
可能不是......除非你需要在Load()
内做一些棘手的事情,这需要一个非const参数。例如,您可能希望std::move(Path)
进入另一个线程。在这种情况下,使用移动语义可能是有意义的。
这是编写rvalue版本方法的正确方法吗?
不,你应该反过来做这件事:
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....
}
答案 3 :(得分:2)
这通常是一个问题,你是否会在内部复制(显式或隐式)传入对象(提供T&&
参数),或者你只是使用它(坚持[const] T&
)
答案 4 :(得分:1)
如果您的Load
成员函数未从传入字符串中分配,则只需提供void Asset::Load(const std::string& Path)
。
如果您确实从传入的path
分配成员变量,那么有一种情况可能会提高void Asset::Load(std::string&& Path)
的效率,但您需要一个不同的实现分配ala loaded_from_path_ = std::move(Path);
。
潜在的好处是调用者,因为&&
版本他们可能会收到成员变量所拥有的免费商店区域,从而避免了该缓冲区的悲观delete[]
离子在void Asset::Load(const std::string& Path)
内部,并且可以在下次分配调用者的字符串时重新分配(假设缓冲区的大小足以适应其下一个值)。
在你陈述的场景中,你通常会传递字符串文字;这样的调用者不会从任何&&
重载中获益,因为没有调用者拥有的std::string
实例来接收现有数据成员的缓冲区。