正确使用右值引用作为参数

时间:2013-03-28 03:43:09

标签: c++ c++11

我们以下面的方法为例:

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版本方法的正确方法吗?

5 个答案:

答案 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实例来接收现有数据成员的缓冲区。