条款25:对右值引用使用std::move,对统一引用使用std::forward

右值引用仅仅绑定到那些准备移动的候选对象上。假如你有个右值引用参数,你应该知道它绑定的对象可以被移动:

class Widget {

public:
    Widget(Widget&& rhs);  // rhs definitely refers to an

    …                                   // object eligible for moving
};

这种情况下,你会希望转移这些对象到其他函数,使用的方式应该允许那些函数利用上对象的右值性。方法就是转换参数绑定到的对象为右值。像条款23解释的,这不仅是std::move所做的,而且就是它为之而生的:

class Widget {
public:
  Widget(Widget&& rhs) // rhs is rvalue reference
    : name(std::move(rhs.name)),
      p(std::move(rhs.p))
  { … }
  …
private:
  std::string name;
  std::shared_ptr<SomeDataStructure> p;
};

另一方面,一个统一引用可能会绑定到一个有资格转移的对象上去。统一引用只有在它们被用右值初始化时才会转换成右值。条款23详细解释了std::forward所做的:

class Widget {
public:
  template<typename T>
  void setName(T&& newName)                // newName is
  { name = std::forward<T>(newName); }  // universal reference
  …
};

简言之,当转发给另外的函数时,右值引用会无条件的转换为右值(通过std::move),因为它们一直绑定到右值上,而统一引用会有条件的转换为右值(通过std::forward),因为它们只是有时候绑定到右值上。

条款23解释了在右值引用上使用std::forward可以表现出正确的行为,但是代码冗长、易错、不符合语言习惯,所以你应该避免对右值引用使用std::forward。甚至对统一引用使用std::move更糟糕,因为那样会产生一些非预期的修改左值(比如局部变量)的影响:

class Widget {
public:
    template<typename T>
    void setName(T&& newName)          // universal reference 统一引用
    { name = std::move(newName); }      // compiles, but is 可以编译,但很糟糕
                                                              … // bad, bad, bad!
private:
    std::string name;
    std::shared_ptr<SomeDataStructure> p;
};


std::string getWidgetName();     // factory function

Widget w;

auto n = getWidgetName();    // n is local variable

w.setName(n);                        // moves n into w!

…                                            // n's value now unknown

这里,局部变量n被传递到了w.setName,这里调用者会认为这是一个对n的只读操作(可以原谅的假设)。但是因为setName函数内部使用了std::move无条件的把其引用参数转换为一个右值,n的值会被移动到w.name中,调用完setName函数返回时,n的值会不可以预测。这样的结果会让调用这绝望甚至疯狂。

你可能认为setName本就不应该声明其参数为一个统一引用。这样的引用不应该是const(见条款24),然而setName也确实不应该修改其参数。你可能发现,假如setName如果简单的重载一个const左值和一个右值函数,整个问题就能得到解决。类似这样:

class Widget {
public:
    void setName(const std::string& newName)      // set from
    { name = newName; }                                        // const lvalue    常量左值

    void setName(std::string&& newName)            // set from
    { name = std::move(newName); }                     // rvalue 非常量右值
    …
};

在这里当然可以正常运行,但是有缺点。第一,有更多的代码需要编写和维护(两个函数而不是一个简单的模板)。第二,效率低下,比如,考虑如下使用setName:

w.setName("Adela Novak");

在使用统一引用做参数的版本里,字面值"Adela Novak"会被传递给setName,然后被传递给std::string(w内部的类型)的赋值操作符。w的name这个成员变量会直接从字面值分配过去,不会产生std::string对象的临时变量。然而,对于重载版本的setName函数,一个临时的std::string对象会产生,用来表示setName的参数所绑定的对象,然后这个临时对象会转移到w的成员数据中。一个对setName的调用会执行3步操作:一个std::string的构造函数(创建临时对象),一个std::string的移动操作符(移动newName到w.name),还有一个std::string的析构函数(销毁临时变量)。这一系列操作确实要比仅仅调用一次std::string的赋值操作符(使用const char* 指针)要昂贵。额外的开销和实现有关,也取决于应用程序和库的实现。但是事实就是用一对重载函数(左值和右值引用)来代替统一引用模板做参数的函数很可能在某些情况下引发效率问题。假如我们更一般化的让这个例子中的Widget的数据成员可以是任意一个类型(而不是已知的std::string),性能差距可能会加大,因为不是所有的类型move操作都像std::string一样消耗小(见条款29)。

然而,重载左值和右值最严重的问题不是代码的尺寸或者不符合语言习惯,也不是运行时效率,而是其不可扩展。Widget::setName仅仅有一个参数,因此两个重载就够了,但是对那些有多个参数的函数,每个参数都有一个左值和一个右值,重载函数的个数会呈几何数增长:n个参数会有 2^n个重载函数。更糟糕的是有些函数(实际上是模板函数)是有无限个参数的,每个参数都可能是左值或右值。这样的函数的典范就是std::make_shared以及C++14的std::make_unique(见条款21),看看他们共同使用重载的函数声明部分:

template<class T, class... Args> // from C++11
shared_ptr<T> make_shared(Args&&... args); // Standard


template<class T, class... Args> // from C++14
unique_ptr<T> make_unique(Args&&... args); // Standard

对这样的函数,重载左值和右值不可选:统一引用才是唯一的道路。我可以向你保证,在这些函数内部,当传递统一引用参数给别的函数时,std::forward会被应用在该统一引用参数上。这才是你需要的。

有些情况下,在一个函数中,你会不止一次的使用那个绑定到右值引用或者统一引用上的对象。你想确保这个值不被转移,直到你完成了对其的操作。这情况下,你会想应用std::move(对右值引用)或者std::forward(对统一引用)到最后的引用的使用上。比如:

template<typename T>       // text is
void setSignText(T&& text) // univ. reference
{
    sign.setText(text);                      // use text, but
                                                  // don't modify it
    auto now =                                 // get current time
    std::chrono::system_clock::now();
    signHistory.add(now,
              std::forward<T>(text));                   // conditionally cast
}                                                                   // text to rvalue

这里,我们想要确保text不被sign.setText修改,因为我们想在调用signHistory.add时使用该值。因此,std::forward的使用仅仅最后使用该统一引用。

对std::move,应用相同的想法(也就是最后时刻将std::move用在右值引用上)。但是注意到在很少的场合下,你会调用 std::move_if_noexcept来代替std::move。想知道何时已经什么原因,查询条款14.

假如你在一个通过传值返回的函数中,你返回一个对象,该对象绑定到一个右值引用或一个统一引用,当你返回引用时你会应用std::move或者std::forward。来看看为什么,考虑一个操作符+函数,把两个矩阵相加,左边一个矩阵已知是个右值(因此它的存储空间可以复用来保存矩阵的合):

Matrix                                                      // by-value return
operator+(Matrix&& lhs, const Matrix& rhs)
{
     lhs += rhs;

    return std::move(lhs);    // move lhs into
}                                         // return value

通过在返回语句中把lhs转换为一个右值(通过std::move),lhs会移动到函数的返回值空间。假如std::move的调用省略了,

Matrix                       // as above
operator+(Matrix&& lhs, const Matrix& rhs)
{
    lhs += rhs;
    return lhs;      // copy lhs into
}                         // return value

事实上lhs是一个左值,会强迫编译器把它拷贝到返回值的位置。假设Matrix类型支持移动拷贝,则会比拷贝构造更有效,于是在返回语句中使用std::move会产生更有效的代码。

假如Matrix不支持移动,转换为右值也没问题,因为右值会简单的通过Matrix的拷贝构造来得到(条款23)。假设Matrix后面调整成支持移动操作,那么下次编译后操作符+会自动受益。这个例子里,应用std::move到函数传值返回的右值引用上,没有任何损失(可能还有提高)。

这个情况对统一引用和std::forward来说很相似。考虑一个函数模板reduceAndCopy,接受一个未约的Fraction对象,约分,然后返回一个约好的值。假如原始的对象是个右值,其值应该移动到返回值上(这样避免了制造一个拷贝的成本),假如原始值是个左值,一个实际的拷贝必须生成。因此,

template<typename T>
Fraction                              // by-value return
reduceAndCopy(T&& frac) // universal reference param
{
    frac.reduce();
    return std::forward<T>(frac);  // move rvalue into return
}                                                  // value, copy lvalue

假如省略了std::forward的调用,则frac会无条件拷贝到reduceAndCopy的返回值。

一些程序员根据上面的信息,试图把它扩展到不适合的场景下。“假如在右值引用参数上使用std::move将拷贝构造转换成移动构造”,他们推理“相同的优化可以用在返回临时变量上”。换句话说,他们推导出一个函数返回一个按值返回的局部变量上,像这样,

Widget makeWidget() // "Copying" version of makeWidget
{
    Widget w;     // local variable
    

    …                 // configure w

    return w;       // "copy" w into return value
}

他们通过转换“拷贝”为“移动”来进行 “优化”:

Widget makeWidget()      // Moving version of makeWidget
{
    Widget w;
    …
    return std::move(w);     // move w into return value
}                                        // (don't do this!)

我用引号的地方表示这个理由是错误的,但为什么是错误的?

错误是因为标准化组织远远领先于那些有如此优化想法的程序员,很久之前就认识到,makeWidget的“拷贝”版本可以通过直接在函数返回值分配的内存上创建变量w来避免拷贝w,这就是我们知道的RVO(返回值优化),C++标准很久之前就一直有了。

本段大意是:RVO说的是编译器可以在一个返回类型为传值的函数里减少对局部对象的拷贝(或者移动)要满足两个条件:1.局部对象的类型和返回类型相同;2.局部对象就是返回值。据此,我们再看看makeWidget的“copy”版本:

Widget makeWidget() // "Copying" version of makeWidget
{
    Widget w;
    …
    return w; // "copy" w into return value
}

这里两个条件都满足了,你要相信我对于这段代码,每一个得体的c++编译器都会应用RVO来避免拷贝w。这意味着那个“copy”版本实际上不拷贝任何东西。

移动版本的makeWidget做了名副其实的事情(假设Widget提供了move构造器):它移动了w的内容到了makeWidget函数的返回值空间。但为什么编译器没有用RVO来消除move,而是在函数返回值分配的内存里重建了一个w。答案很简单:它们不会。条件2规定了RVO只能在返回值是一个局部对象是执行,而不是move版本里所做的。再看看它的返回语句:

return std::move(w);

这里返回的不是局部变量w,而是它的引用--std::move(w)的返回值。返回一个局部对象的引用不满足RVO的条件,因此编译器必须把w移动到函数返回值的位置。开发者试图通过应用std::move到一个即将返回的局部变量帮助编译器优化,实际上是限制了编译优化。

但是RVO是一个优化。编译器不需要考虑省略拷贝和移动操作,甚至在它们被允许的时候。也许你是偏执狂,你担心编译器会通过拷贝操作来惩罚你,仅仅因为它们可以如此。 Or
perhaps you’re insightful enough to recognize that there are cases where the RVO is
difficult for compilers to implement, e.g., when different control paths in a function
return different local variables. (Compilers would have to generate code to construct
the appropriate local variable in the memory allotted for the function’s return value,
but how could compilers determine which local variable would be appropriate?) If so,
you might be willing to pay the price of a move as insurance against the cost of a
copy. That is, you might still think it’s reasonable to apply std::move to a local
object you’re returning, simply because you’d rest easy knowing you’d never pay for a
copy.

在那个场景下,把std::move应用到一个局部对象依旧是个坏主意。The part
of the Standard blessing the RVO goes on to say that if the conditions for the RVO
are met, but compilers choose not to perform copy elision, the object being returned
must be treated as an rvalue. In effect, the Standard requires that when the RVO is
permitted, either copy elision takes place or std::move is implicitly applied to local
objects being returned. So in the “copying” version of makeWidget,
Widget makeWidget() // as before
{
Widget w;

return w;
}
compilers must either elide the copying of w or they must treat the function as if it
were written like this:
Widget makeWidget()
{
Widget w;

return std::move(w); // treat w as rvalue, because
} // no copy elision was performed
The situation is similar for by-value function parameters. They’re not eligible for
copy elision with respect to their function’s return value, but compilers must treat
them as rvalues if they’re returned. As a result, if your source code looks like this,
Widget makeWidget(Widget w) // by-value parameter of same
{ // type as function's return

return w;
}
compilers must treat it as if it had been written this way:
Widget makeWidget(Widget w)
{

return std::move(w); // treat w as rvalue
}
This means that if you use std::move on a local object being returned from a func‐
tion that’s returning by value, you can’t help your compilers (they have to treat the
local object as an rvalue if they don’t perform copy elision), but you can certainly hin‐
der them (by precluding the RVO). There are situations where applying std::move
to a local variable can be a reasonable thing to do (i.e., when you’re passing it to a
function and you know you won’t be using the variable any longer), but as part of a
return statement that would otherwise qualify for the RVO or that returns a by-
value parameter isn’t among them.
                                               要记住的事情:
1.在每次最后一次使用时,对右值引用应用std::move,对统一引用应用std::forward;
2.函数按值返回右值引用和统一引用时,相同处理;
3.绝不要对局部对象使用std::move和std::forward,假如它们另外合乎返回值优化策略。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值