Item 25: 对右值引用使用std::move,对universal引用则应用std::forward

正文翻译由《effective modern
C++》,由于水平有限,故不能确保翻译完全正确,欢迎指出错误。谢谢!

博客已经搬迁到这里啦

右值引用只好绑定那么些有身份为move的目的上。要是你生出一个右值引用类型的参数,你尽管知道之于绑定的目的足以叫move:

class Wdiget{
    Widget(Widget&& rhs);   // rhs肯定指向一个有资格被move的对象
    ...
};

每当那种场所下,你会合想念传这样一个目的为任何函数,来允许这个函数能以对象的右值属性。为了达到如此的目的,需要将绑定到这么些目的的参数转换成为右值。就比如Item
23讲的那么,std::move不仅是这样做了,它便是为是目标而受创立出的:

class Widget{
public:
    Widget(Widget&& rhs)            // rhs是一个右值引用
    : name(std::move(rhs.name)),
      p(std::move(rhs.p))
      {...}
     ...

private:
    std::string name;
    std::shared_ptr<SomeDataStructure> p;
};

以一派,一个universal引用可能(译注:只是可能不是毫无疑问)被绑定到一个发身份为move的靶子上。universal引用只当其由右值伊始化的上需要为撤换成为一个右值。Item
23说了登时虽是std::forward具体做的政工:

class Widget {
public:
    template<typename T>
    void setName(T&& newName)               // newName是一个
    { name = std::forward<T>(newName); }    // universal引用

    ...
};

总而言之,因为右值引用总是被绑定到右手值,所以当它被转发让其它函数的上,应该叫白白地转换成为右值(通过std::move),而universal引用由于只是不定时地为绑定到右值,所以当倒车它们常,它们当于发标准化地更换成为右值(通过std::forward)。

Item
23表达了针对右值引用使用std::forward能吃她显得出科学的所作所为,可是源代码会由此变得长、易错、不入习惯的,所以您该避免对右值引用使用std::forward。对universal引用使用std::move是进一步糟糕的想法,因为这么会对左值(比如,局部变量)爆发非预期的改:

class Widget {
public:
    template<typename T>
    void setName(T&& newName)       // universal引用
    { name = std::move(newName); }  // 能通过编译,但是
    ...                             // 这代码太糟糕了

private:
    std::string name;
    std::shared_ptr<SomeDataStructure> p;
};

std::string getWidgetName();        // 工厂函数

Widget w;                           

auto n = getWidgetName();           // n是局部变量

w.setName(n);                       // 把n move到w中去!

...                                 // n的值现在是未知的

此地,局部变量n被染于w.setName,调用者完全可以如果这是一个对n只读的操作。不过因为stdName在其间会用了std::move,然后无条件地拿他的援参数转换成了右值,所以n的值将为move到w.name中失,最终在setNamen调用完成之后,n将化一个不解之值。这样的一言一行会吃调用者很丧气,甚至会气得黄键盘!

卿可能提出stdName不应讲明其的参数为universal引用。固然如此的援不可知是const的(看Item
24,译注:加const就变成右值引用了),可是steName确实不应改其的参数。你还可能指出如setName使用const
左值和右值举办重载,整个问题拿给免。像是如此:

class Widget {
public:
    void setName(const std::string& newName)    // 从const左值来set
    { name = newName; }

    void setName(std::string&& newName)         // 从右值来set
    { name = std::move(newName); }

    ...
};

在这种气象下,确实能干活,不过这种措施是发出欠点的。首先,它多了来代码里而修和保障的代码量(使用简单个函数代替一个简单易行的模板)。其次,它进一步低效。举个例子,考虑这setName的拔取:

w.setName("Adela Novak");

运universal引用版本的setName,在“AdelaNovak”字符串于染于setName时,它会为转正让处于w对象中的一个std::string(就是w.name)的operator=(译注:const
char本子的operator=)函数。由此,w的name数据成员将凡用字符串间接赋值的;没有起一个现之std::string对象。但是,使用重载版本的setName,为了为setName的参数能绑定上去,一个临时的std::string对象将吃创设,然后是临时之std::string对象将给移动到w的多少成员中失。因而那setName的调用需要执行同样坏std::string的构造函数(为了创设临时对象),一个std::string的move
operator=(为了move
newName到w.name中失),以及一个std::string的析构函数(为了销毁临时对象)。对于const
char

指针来说,比从才调用std::string的operator=,下面这么些函数就是多花的代价。额外的代价来或乘实现的不比而生变化,并且代价是否值得考虑呢以趁着以和函数库底差而来变化。不管怎么说,事实就是是,在有的景观下,使用同一针对性重复载了左值和右值的函数来替换带universal引用参数的函数模板来或扩张运行期的代价。假若我们放者例子,使得Widget的数量成员可以是轻易档次的(不仅仅是熟悉的std::string),性能的落差将重新不行,因为无是具备品类的move操作都和std::string一样好的(看Item
29)。

但是,关于重载左值和右值最要害的题材未在源代码的体积与运用习惯,也不在于执行期的频率。而在于她是同种植而扩展性很不同的规划。Widget::setName只带一个参数,所以才需要少单重载,可是对于一些带走更多参数的函数,而且每个参数还足以是左值或右值,那么得重载的数据就是成几哪增长了:n个参数需要2^n个重载。并且及时还免是无限不佳的。一些函数—函数模板—带领不确定数量的参数,每个参数可以是左值或右值。这种函数的表示人固然是std::make_shared,以及C++14中的std::make_unique(看Item
21)。看一下她最普遍的声明式:

template<class T, class... Args>
shared_ptr<T> make_shared(Args&&... args);      // 来自C++11标准库

template<class T, class... Args>
unique_ptr<T> make_shared(Args&&... args);      // 来自C++14标准库

于这样的函数,重载左值和右值的方无适用了,所以universal引用成为了唯一的解决方案。并且我可为您担保,在这一个函数内部,当universal引用被污染给此外函数的时刻,使用的凡std::forward。这为是您当做的业务。

刚巧开接触这几个的时候不是异常有必不可少,然而最终,你到底可以遇见以一部分景观,对于让绑定到右值引用或universal的援的对象,你以于一个函数中行使她抢先同样潮,而且你想管在你以完毕它们前,它们不晤面吃move走。对于这种场合,你可独自于末一不善以这多少个引用的时节长std::move(对于右值引用)或std::forward(对于universal引用)。举个例子:

template<typename T>
void setSignText(T&& text)                  // text是universal引用
{
    sign.setText(text);                     // 使用text,但是不修改它

    auto now =                              // 获得当前时间
        std::chrono::system_clock::now();   

    signHistory.add(now,
                    std::forward<T>(text)); // 有条件地把text转换
}                                           // 为右值

这里,我们回忆要包text的价没有于sign.setText改变。因为我们当调用signHistory.add的时段还惦念只要运用是价值。因而只在末动用universal引用的时节才对这利用std::forward。

C++,于std::move,概念是一致之(也虽然是,只以最后使右值引用的下才对这接纳std::move),可是我们发出必要注意一个未平凡的景色,这种状态下您以会见调用std::move_if_noexcept来代表std::move。想了解啊时候和为啥,请看Item
14。

设当一个再次来到值是传值(by-value)的函数中,你想回一个对象,而且以此目的为绑定到一个右值引用或universal引用上去了,那么当您回去引用的时,你汇合想对该动std::move或std::forward。为了验证这种情景,考虑一个operator+函数,它将个别只矩形加在一起,右侧的矩阵是一个右值(由此我们得以为它们的内存空间用来存放矩阵的和):

Matrix                                      // 通过传值返回
operator+(Matrix&& lhs, const Matrix& rhs)  
{   
    lhs += rhs;
    return std::move(lhs);                  // 把lhs move到返回值中去
}

经过当回语句被管lhs转换为一个右值(通过std::move),lhs将吃move到函数的重临值所在的内存区域。假诺未调用std::move,

Matrix                                      // 同上
operator+(Matrix&& lhs, const Matrix& rhs)  
{   
    lhs += rhs;
    return lhs;                             // 把lhs拷贝到返回值中去
}

这样的话,lhs是一个左值,并且以强制编译器把她的价拷贝到重回值所在的内存区域。假若Matrix类型匡助move操作,而move操作以于拷贝操作逾高效,所以下于回语句中std::move将时有暴发重复快捷之代码。

假若Matrix不援助move操作,把她换成右值未碰面促成什么麻烦,因为右值将略地由此拷贝构造函数被拷贝过去(看Item
23)。假如Matrix之后被改动,由此会支撑move操作了,operator+在生同样潮编译过后拿活动升级其的效用。这即是自我假使说的情状了,当函数通过是传值重返时,通过以std::move把要回到的值转换成右值,大家以不会晤损失外东西(却来或拿到多)。

那种状态和universal引用和std::forward是一般的。考虑一个函数模板reduceAndCopy,它或许为一个未reduce的Fraction对象作为参数,在函数中reduce它,然后回到一个reduce过后底拷贝值。如若源对象是一个右值,它的价应叫move到回值中(由此防止了同样不佳拷贝的代价),可是只要果源对象是一个左值,一个拷贝值将被成立。由此:

template<typename T>
Fraction                                // 通过传值返回
reduceAndCopy(T&& frac)                 // universal引用参数
{
    frac.reduce();
    return std::forward<T>(frac);       // 把右值move到返回值中
}                                       // 把左值copy到返回值中

假诺未动std::forward调用,frac将于白白地拷贝到reduceAndCopy的归值备受失。

部分程序员吸收了面的知后会合尝试在去管她增加及另外境况遇去,可是当这多少个情状下是匪该这么做的。“假使对一个假如于拷贝到回值备受失之右值引用参数使用std::move,能管copy构造函数转换成move构造函数,”他们便会面想,“那么自己可以针对用被归的有的变量执行同样的优化。”不问可知,他们觉得,假如为有的函数重返一个传值的片变量,比如这样:

Widget makeWidget()         // 拷贝版本的makeWidget
{
    Widget w;               // 局部变量

    ...                     // 配置w

    return w;               // 拷贝w到返回值中去
}

她俩虽可知由此把“拷贝”转换成为move来“优化”它:

Widget makeWidget()         // 拷贝版本的makeWidget
{
    Widget w;               
    ...                     
    return std::move(w);    // 把w move到返回值中去
}                           // (不要这么做!)

本身慷慨之笺注应该已经指示您那么些推导过程是生问题之。但是她干吗起题目吧?

及时是坐,对于这种优化,C++标准委员会早以这个程序员以前就是提出了。很早从前大家还公认的相同桩事:makeWidgetde
“拷贝”版本会免拷贝局部变量w,只需要通过在内存中协会其并分配给函数的重返值即可。那即是豪门熟谙的RVO(return
value
optimization,重返值优化),因为专业被曾经发出一个了,所以她于C++的正儿八经公开爱抚了。

规定如此一个护卫是可怜麻烦的办事,因为您只有想假设在未会师潜移默化及软件之所作所为时才允许那样消除拷贝。把标准中原来的(这多少个本来的条条框框相比arguably
toxic,
译注:那多少个照字面来翻译是:可以说凡是爆发毒的,可以了解呢是负面的)规则举办转移写之后,这么些特其余维护告诉我们,在再次来到值是传值的函数中,只要你成功:(1)局部对象的连串和函数重临值的品类一样(2)这么些片对象将吃归,编译器就起或裁撤一个片段对象的正片(或move)。带在这个极,让我们看一下makeWidget的“拷贝”版本:

Widget makeWidget()         // 拷贝版本的makeWidget
{
    Widget w;               
    ...                     
    return w;               // 拷贝w到返回值中去
}

鲜只尺码在此地还满意了,所以恳请相信我,对于这段代码,每个正常的C++编译器都汇合采纳RVO来制止w的正片。这意味makeWidget的“拷贝”版本事实上不会合拷贝任何东西。

makeWidget的move版本只开其名字所说的事物(假使Widget提供一个move构造函数):它把w的内容move到makeWidget的重临值所在的内存中失。然则为何比编译器不利用RVO来驱除move操作,在内存中协会一个w分配给函数的再次回到值的吧?回答很简单:它们不可能这么做。情形(2)规定了RVO只有以回到的值是局部对象时才实施,可是makeWidget的move版本不是如此做的。再看一下它的回语句:

return std::move(w);

此处归的匪是一对对象w,它是一个w的援—std::move(w)的归来值。重回一个有些对象的援不克满意RVO的准绳要求,所以编译器必须把w
move到函数的再次来到值所在的内存中失去。开发者试图对将要重临的有变量调用std::move,来匡助他们的编译器举行优化,可是就正限制了他们的编译器的优化能力!

可RVO只是一个优化。甚至当她们为允许这样做时,编译器也不是早晚要解除拷贝和move操作的。可能您发出硌情感障碍,并且你担心若的编译器会就此拷贝操作惩罚你,只是因为它能这么做。或者可能你有充分的知可以亮,一些情下之RVO对于编译器来说是生麻烦实现之,比如,在一个函数中,不同的支配路径再次回到不同的片段变量。(编译器将必须有相应的代码,在内存中布局合适的一些变量分配为函数的回值,可是编译器怎么领悟哪位局部变量是适用的为?)即便这样,你或许愿意交move的代价来确保非会合生出拷贝所欲的费。也就是说,因为你精晓你永远不需要交拷贝的代价,所以若或许要觉得,把std::move用在你若回来的一对对象及是客观的。

在这种情形下,把std::move用当有对象及或者一个坏的注意。标准中有关RVO的一部分还说到,遇到能展开RVO优化的情事,尽管编译器采用无错过排除拷贝,则被归的靶子要于视为一个右值。实际上,C++标准要求当RVO被允许时,要么消除拷贝,要么隐式地拿std::move用在如再次来到的局部对象上。所以于makeWidget的“拷贝”版本被,

Widget makeWidget()         // 同上
{
    Widget w;               
    ...                     
    return w;               
}

编译器必须要消除掉w的正片,要么把函数看成这样子:

Widget makeWidget()
{
    Widget w;               
    ...                     
    return std::move(w);        //由于没有消除拷贝,所以把w视为右值
}

这种场馆以及函数参数是传值的情状是一样的。对于这么些函数的再次回到值而言,它们不入消除拷贝的法,可是假如她叫归,编译器必须管其就是右值。假诺源代码看起像是这般:

Widget makeWidget(Widget w)     // 传值的参数和函数的返回值类型一致
{                           
    ...
    return w;
}   

编译器必须管函数视为这样:

Widget makeWidget(Widget w) 
{                           
    ...
    return std::move(w);        //把w视为右值
}   

即刻意味,在一个再次回到值是传值的函数中,尽管您对一个如回的一些对象下std::move,那么您切莫会晤扶助到公的编译器(如若其不排拷贝的话,它们要将有些对象视为右值),可是你一定有或阻碍其的优化(阻碍了RVO优化)。当把std::move用当一些变量时,有几乎种植意况是比客观之(也不怕是,当你管其污染于一个函数,并且你知你切莫汇合另行以这些变量时),然则于暴发身份举办RVO优化或者重返一个传值参数的归来语句被,它(调用std::move)是未适用的。

            你如牢记的从事
  • 在末一不好采纳时,再把std::move用在右值引用上,把std::forward用当universal引用上。
  • 于一个再次回到值是传值的函数,对于假如于归的右值引用和universal引用做一样的事情(把std::move用在右值引用上,把std::forward用当universal引用上)。
  • 倘若局部变量有资格举办RVO优化,不要将std::move或std::forward用在那多少个有变量中。