#Item 23:Understand std::move and std::forward
首先通过了解它们(指std::move和std::forward)不做什么来认识std::move和std::forward是非常有用的。std::move不move任何东西。std::forward也不转发任何东西。在运行时,他们什么都不做。不产生可执行代码,一个比特/Users/shikunfeng/Documents/neteaseWork/timeline_15_05_18/src/main/webapp/tmpl/web2/widget/event2.ftl的代码也不产生。
std::move和std::forward只是执行转换的函数(确切的说应该是函数模板)。std::move无条件的将它的参数转换成一个右值,而std::forward当特定的条件满足时,才会执行它的转换。这就是它们本来的样子.这样的解释产生了一些新问题,但是,基本上,就是这么一回事。
为了让这个故事显得更加具体,下面是C++ 11的std::move的一种实现样例,虽然不能完全符合标准的细节,但也非常相近了。
```cpp
template<typename T>
typename remove_reference<T>::type&&
move(T&& param)
{
using ReturnType = //alias declaration;
typename remove_reference<T>::type&&;//see Item 9
return static_cast<ReturnType>(param);
}
```
我为你高亮的两处代码(我做不到啊!--菜b的译者注)。首先是函数的名字move,因为返回的类型非常具有迷惑性,我可不想让你一开始就晕头转向。另外一处是最后的转换,包含了move函数的本质。正如你所看到的,std::move接受了一个对象的引用做参数(准确的来说,应该是一个universal reference.请看Item 24。这个参数的格式是T&& param,但是请不要误解为move接受的参数类型就是右值引用,请继续往下看----菜b译者注),并且返回指向同一个对象的引用。
函数返回值的"&&"部分表明std::move返回的是一个右值引用。但是呢,正如Item 28条解释的那样,如果T的类型恰好是一个左值引用,T&&的类型就会也会是左值引用。为了阻止这种事情的发生,我们用到了type trait(请看Item 9),在T上面应用std::remove_reference,它的效果就是“去除”T身上的引用,因此保证了"&&"应用到了一个非引用的类型上面。这就确保了std::move真正的返回的是一个右值引用(rvalue reference),这很重要,因为函数返回的rvalue reference就是右值(rvalue).因此,std::move就做了一件事情:将它的参数转换成了右值(rvalue).
说一句题外话,std::move可以更优雅的在C++14中实现。感谢返回函数类型推导(function return type deduction 请看Item 3),感谢标准库模板别名(alias template)`std::remove_reference_t`(请看Item 9),`std::move`可以这样写:
```cpp
template<typename T> //C++14; still in
decltype(auto) move(T && param) //namespace std
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}
```
看起来舒服多了,不是吗?
因为std::move除了将它的参数转换成右值外什么都不做,所以有人说应该给它换个名字,比如说叫`rvalue_cast`可能会好些。话虽如此,它现在的名字仍然就是`std::move`.所以记住`std::move`做什么不做什么很重要。它只作转换,不做move.
当然了,rvalues是对之执行move的合格候选者,所以对一个对象应用std::move告诉编译器,该对象很合适对之执行move操作,所以std::move的名字就有意义了:标示出那些可以对之执行move的对象。
事实上,rvalues并不总是对之执行move的合格候选者。假设你正在写一个类,它用来表示注释。此类的构造函数接受一个包含注释的std::string做参数,并且将此参数的值拷贝到一个数据成员上.受到Item 41的影响,你声明一个接收by-value参数的构造函数:
```cpp
class Annotation{
public:
explicit Annotation(std::string text);//param to be copied,
... //so per Item 41, pass by value
};
```
但是Annotation的构造函数只需要读取text的值。并不需要修改它。根据一个历史悠久的传统:能使用const的时候尽量使用。你修改了构造函数的声明,text改为const:
```cpp
class Annotation{
public:
explicit Annotation(const std::string text);//param to be copied,
... //so per Item 41, pass by value
};
```
为了避免拷贝text到对象成员变量带来拷贝代价。你继续忠实于Item 41的建议,对text应用std::move,因此产生出一个rvalue:
```cpp
class Annotation{
public:
explicit Annotation(const std::string text)
: value(std::move(text))//"move" text into value; this code
{...} //doesn't do what it seems to!
...
private:
std::string value;
};
```
这样的代码通过了编译,链接,最后运行。而且把成员变量value设置成text的值。代码跟你想象中的完美情况唯一不同的一点是,它没有对text执行move到value,而是拷贝了text的值到value.text确实被std::move转化成了rvalue,但是text被声明为const std::string.所以在cast之前,text是一个const std::string类型的lvalue.cast的结果是一个const std::string的rvalue,但是自始至终,const的性质一直没变。
代码运行时,编译器要选择一个std::string的构造函数来调用。有以下两种可能:
```cpp
class string{ //std::string is actually a
public: //typedef for std::basic_string<char>
...
string(const string& rhs); //copy ctor
string(string&& rhs); //move ctor
};
```
在Annotation的构造函数的成员初始化列表(member initialization list),`std::move(text)`的结果是const std::string的rvalue.这个rvalue不能传递给std::string的move构造函数,因为move构造函数接收的是非const的std::string的rvalue引用。然而,因为lvalue-reference-to-const的参数类型可以被const rvalue匹配上,所以rvalue可以被传递给拷贝构造函数.因此即使text被转换成了rvalue,上文中的成员初始化仍调用了std::string的拷贝构造函数!这样的行为对于保持const的正确性是必须的。从一个对象里move出一个值通常会改变这个对象,所以语言不允许将const对象传递给像move constructor这样的会改变次对象的函数。
从本例中你可以学到两点。首先,如果你想对这些对象执行move操作,就不要把它们声明为const.对const对象的move请求通常会悄悄的执行到copy操作上。
std::forward的情况和std::move类似,但是和std::move__无条件地__将它的参数转化为rvalue不同,std::forward在特定的条件下才会执行转化。std::forward是一个__有条件__的转化。为了理解它何时转化何时不转化,我们来回想一下std::forward的典型的使用场景。最常见的场景是:一个函数模板(function template)接受一个universal reference参数,将它传递给另外一个函数(作参数):
```cpp
void process(const Widget& lvalArg); //process lvalues
void process(Widget&& rvalArg); //process rvalues
template<typename T>
void logAndProcess(T&& param) //template that passes
//param to process
{
auto now = std::chrono::system_clock::now(); //get current time
makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}
```
请看下面对logAndProcess的两个调用,一个使用的lvalue,另一个使用的rvalue:
```cpp
Widget w;
logAndProcess(w); //call with lvalue
logAndProcess(std::move(w)); //call with rvalue
```
在logAndProcess的实现中,参数param被传递给了函数process.process按照参数类型是lvalue或者rvalue都做了重载。当我们用lvalue调用logAndProcess时,我们自然地期望:forward给process的也是一个lvalue,当我们用rvalue来调用logAndProcess时,我们希望process的rvalue重载版本被调用。
但是就像所有函数的参数一样,param可能是一个lvalue.logAndProcess内的每一个对process的调用因此想要调用process的lvalue重载版本。为了让以上代码的行为表现正确,我们需要一个机制,param转化为rvalue当且仅当:传递给logAndProcess的用来初始化param的参数必须是一个rvalue.这正是std::forward做的事情。这就是为什么std::forward被称作是一个__条件__转化(conditional cast):当参数被rvalue初始化时,才将参数转化为rvalue.
你可能想知道std::forward怎么知道它的参数是否被一个rvalue初始化。比如说,在以上的代码中,std::forward怎么知道param被一个lvalue或者rvalue初始化?答案很简单,这个信息蕴涵在logAndProcess的模板参数T中。这个参数传递给了std::forward,然后std::forward来从中解码出此信息。欲知详情,请参考Item 28。
std::move和std::forward都可以归之为cast.唯一的一点不同是,std::move总是在执行casts,而std::forward是在某些条件满足时才做。你可能觉得我们不用std::move,只使用std::forward会不会好一些。从一个纯粹是技术的角度来说,答案是肯定的:std::forward是可以都做了,std::move不是必须的。当然,可以说这两个函数都不是必须的,因为我们可以在任何地方都直接写cast代码,但是我希望我们在此达成共识:这样做很恶心。
std::move的魅力在于:方便,减少了错误的概率,而且更加简洁。举个栗子,有这样的一个class,我们想要跟踪,它的move构造函数被调用了多少次,我们这次需要的是一个static的counter,它在每次move构造函数被调用时递增。假设该class还有一个std::string类型的非静态成员,下面是一个实现move constructor(使用std::move)的常见的例子:
```cpp
class Widget{
public:
Widget(Widget&& rhs)
: s(std::move(rhs.s))
{ ++moveCtorCalls; }
...
private:
static std::size_t moveCtorCalls;
std::string s;
}
```
如果要使用std::forward来实现同样的行为,代码像下面这样写:
```cpp
class Widget{
public:
Widget(Widget&& rhs) //unconventional,
: s(std::forward<std::string>(rhs.s)) //undesirable
{ ++moveCtorCalls; } //implementation
...
}
```
请注意到:首先,std::move只需要一个函数参数(rhs.s), std::forward不只需要一个函数参数(rhs.s),还需要一个模板类型参数(std::string).然后,注意到我们传递给std::forward的类型是非引用类型(non-reference),因为这就意味着传递的那个参数是一个rvalue(请看Item 28)。综上,这就意味着std::move比std::forward用起来更方便(至少少敲了不少字),免去了让我们传递一个表示函数参数是否是一个rvalue的类型参数。消除了传递错误类型(比如说,传一个std::string&,可以导致数据成员s被拷贝构造,而不是想要的move构造)的可能性。
更重要的是,std::move的使用表明了对rvalue的无条件的转换,然而,当std::forward只对被绑定了rvalue的reference进行转换。这是两个非常不同的行为。std::move就是为了move操作而生,而std::forward,就是将一个对象转发(或者说传递)给另外一个函数,同时保留此对象的左值性或右值性(lvalueness or rvalueness)。所以我们需要这两个不同的函数(并且是不同的函数名字)来区分这两个操作。
|要记住的东西|
|:--------- |
|std::move执行一个无条件的对rvalue的转化。对于它自己本身来说,它不会move任何东西|
|std::forward在参数被绑定为rvalue的情况下才会将它转化为rvalue|
|std::move和std::forward在runtime时啥都不做|
- 出版者的忠告
- 致谢
- 简介
- 第一章 类型推导
- 条款1:理解模板类型推导
- 条款2:理解auto类型推导
- 条款3:理解decltype
- 条款4:知道如何查看类型推导
- 第二章 auto关键字
- 条款5:优先使用auto而非显式类型声明
- 条款6:当auto推导出非预期类型时应当使用显式的类型初始化
- 第三章 使用现代C++
- 条款7:创建对象时区分()和{}
- 条款8:优先使用nullptr而不是0或者NULL
- 条款9:优先使用声明别名而不是typedef
- 条款10:优先使用作用域限制的enmu而不是无作用域的enum
- 条款11:优先使用delete关键字删除函数而不是private却又不实现的函数
- 条款12:使用override关键字声明覆盖的函数
- 条款13:优先使用const_iterator而不是iterator
- 条款14:使用noexcept修饰不想抛出异常的函数
- 条款15:尽可能的使用constexpr
- 条款16:保证const成员函数线程安全
- 条款17:理解特殊成员函数的生成
- 第四章 智能指针
- 条款18:使用std::unique_ptr管理独占资源
- 条款19:使用std::shared_ptr管理共享资源
- 条款20:在std::shared_ptr类似指针可以悬挂时使用std::weak_ptr
- 条款21:优先使用std::make_unique和std::make_shared而不是直接使用new
- 条款22:当使用Pimpl的时候在实现文件中定义特殊的成员函数
- 第五章 右值引用、移动语义和完美转发
- 条款23:理解std::move和std::forward
- 条款24:区分通用引用和右值引用
- 条款25:在右值引用上使用std::move 在通用引用上使用std::forward
- 条款26:避免在通用引用上重定义函数
- 条款27:熟悉通用引用上重定义函数的其他选择
- 条款28:理解引用折叠
- 条款29:假定移动操作不存在,不廉价,不使用
- 条款30:熟悉完美转发和失败的情况
- 第六章 Lambda表达式
- 条款31:避免默认的参数捕捉
- 条款32:使用init捕捉来移动对象到闭包
- 条款33:在auto&&参数上使用decltype当std::forward auto&&参数
- 条款34:优先使用lambda而不是std::bind
- 第七章 并发API
- 条款35:优先使用task-based而不是thread-based
- 条款36:当异步是必要的时声明std::launch::async
- 条款37:使得std::thread在所有的路径下无法join
- 条款38:注意线程句柄析构的行为
- 条款39:考虑在一次性事件通信上void的特性
- 条款40:在并发时使用std::atomic 在特殊内存上使用volatile
- 第八章 改进
- 条款41:考虑对拷贝参数按值传递移动廉价,那就尽量拷贝
- 条款42:考虑使用emplace代替insert