67

我们以下面的方法为例:

void Asset::Load( const std::string& path )
{
    // complicated method....
}

这种方法的一般用途如下:

Asset exampleAsset;
exampleAsset.Load("image0.png");

既然我们知道大多数时候 Path 是一个临时的右值,那么添加这个方法的右值版本有意义吗?如果是这样,这是一个正确的实现吗?

void Asset::Load( const std::string& path )
{
    // complicated method....
}
void Asset::Load( std::string&& path )
{
     Load(path); // call the above method
}

这是编写方法的右值版本的正确方法吗?

4

6 回答 6

79

对于您的特定情况,第二个重载是无用的。

使用只有一个重载 for 的原始代码,Load该函数被调用用于左值和右值。

使用新代码,第一个重载是为左值调用的,第二个是为右值调用的。但是,第二个重载调用第一个。最后,调用一个或另一个的效果意味着将执行相同的操作(无论第一个重载做什么)。

因此,原代码和新代码的效果是一样的,只是第一个代码更简单。

决定一个函数是否必须按值、左值引用或右值引用来接受参数,很大程度上取决于它的作用。当您想要移动传递的参数时,您应该提供一个采用右值引用的重载。那里有几个关于移动语义的很好的参考资料,所以我不会在这里介绍。

奖金

为了帮助我表达我的观点,请考虑这个简单的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一个副本。因此是第二条输出线。qpf

现在,考虑p按值取值,即f改为:

void f(probe p) {
    // use p;
}

现在的输出f("foo");

ctr

在这种情况下,有些人会感到惊讶:没有副本!通常,如果您通过引用获取参数并将其复制到函数中,那么最好按值获取参数。p在这种情况下,编译器可以直接从输入( )构造参数(在这种情况下),而不是创建一个临时文件并复制它"foo"。有关更多信息,请参阅想要速度?按值传递。戴夫亚伯拉罕斯。

该指南有两个值得注意的例外:构造函数和赋值运算符。

考虑这个类:

struct foo {
    probe p;
    foo(const probe& q) : p(q) { }
};

构造函数接受一个probeby const 引用,然后将其复制到p. 在这种情况下,遵循上述指南不会带来任何性能改进,并且probe无论如何都会调用 ' 的复制构造函数。但是,q按值取值可能会产生一个重载解决问题,类似于我现在将介绍的带有赋值运算符的问题。

假设我们的类probe有一个非抛出swap方法。那么其赋值运算符的建议实现(暂时用 C++03 术语思考)是

probe& operator =(const probe& other) {
    probe tmp(other);
    swap(tmp);
    return *this;
}

然后,按照上面的指导,最好这样写

probe& operator =(probe tmp) {
    swap(tmp);
    return *this;
}

现在进入带有右值引用和移动语义的 C++11。您决定添加一个移动赋值运算符:

probe& operator =(probe&&);

现在在临时上调用赋值运算符会产生歧义,因为两个重载都是可行的,并且没有一个优先于另一个。要解决此问题,请使用赋值运算符的原始实现(通过 const 引用获取参数)。

实际上,这个问题并不是构造函数和赋值运算符所特有的,任何函数都可能发生。(不过,您更有可能使用构造函数和赋值运算符来体验它。)例如,调用g("foo");wheng具有以下两个重载会引起歧义:

void g(probe);
void g(probe&&);
于 2013-03-28T04:55:40.787 回答
8

除非您正在做的不是调用 的左值引用版本Load,否则您不需要第二个函数,因为右值将绑定到 const 左值引用。

于 2013-03-28T04:11:19.967 回答
5

既然我们知道大多数时候 Path 是一个临时的右值,那么添加这个方法的右值版本有意义吗?

可能不会......除非你需要在内部做一些Load()需要非常量参数的棘手事情。例如,也许你想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....
}
于 2016-11-22T08:36:25.587 回答
1

这通常是一个问题,您是否会在内部复制(显式或隐式)传入对象(提供T&&参数),或者您将只使用它(坚持[const] T&)。

于 2013-03-28T18:29:01.980 回答
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实例来接收现有数据成员的缓冲区。

于 2015-05-19T04:02:39.397 回答
0

这是我在尝试决定函数签名时所做的事情

  1. (const std::string& const_lvalue)参数是只读的
  2. (std::string& lvalue)我可以修改参数(通常在 中放一些东西),所以更改对调用者来说是可见的
  3. (std::string&& rvalue)我可以修改参数(通常从 中窃取一些东西),零后果,因为调用者将不再看到/使用这个参数(考虑它在函数返回后自毁)

这三个都是“通过引用传递”,但它们表现出不同的意图。2+3 类似,它们都可以修改参数,但 2 希望调用者看到修改,而 3 则不会。

// (2) caller sees the change argument
void ModifyInPlace(Foo& lvalue){
  delete lvalue.data_pointer;
  lvalue.data_pointer = nullptr; 
}

// (3) move constructor, caller ignores the change to the argument
Foo(Foo&& rvalue)
{
  this->data_pointer = that.data_pointer;
  that.data_pointer = nullptr;
}

于 2021-10-20T07:31:27.740 回答