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

博客已经迁移到这里啦

根据std::move和std::forward不能做什么来熟悉它们是一个好办法。std::move没有move任何东西,std::forward没有转发任何东西。在运行期,它们没有做任何事情。它们没有产生需要执行的代码,一byte都没有。

std::move和std::forward只不过就是执行cast的两个函数(实际上是函数模板)。std::move无条件地把它的参数转换成一个右值,而std::forward只在特定条件满足的情况下执行这个转换。就是这样了,我的解释又引申出一系列的新问题,但是,基本上来说,上面说的就是全部内容了。

为了让内容更加形象,这里给出C++11中std::move实现的一个例子。它没有完全遵循标准的细节,但是很接近了。

template<typename T>								//在命名空间std中
typename remove_reference<T>::type&&
move(T&& param)
{
using ReturnType = //别名声明
typename remove_reference<T>::type&&; //看Item 9 return static_cast<ReturnType>(param);
}

我已经帮你把代码的两个部分高亮(move和static_cast)显示了。一个是函数的名字,因为返回值类型挺复杂的,我不想让你在这复杂的地方浪费时间。另一个地方是包括了这个函数的本质(cast)。就像你看到的那样,std::move需要一个对象的引用(准确地说是一个universal引用,看Item 24),并且返回同一个对象的引用。

函数返回值类型的“&&”部分暗示了std::move返回一个右值引用,但是,就像Item 28解释的那样,如果类型T恰好是左值引用,T&&将成为一个左值引用。为了防止这样的事情发生,type trait(看Item 9)std::remove_reference被用在T上了,因此能保证把“&&”加在不是引用的类型上。这样能保证让std::move确切地返回一个右值引用,并且这是很重要的,因为由函数返回的右值引用是一个右值。因此,std::move所做的所有事情就是转换它的参数为一个右值。

说句题外话,在C++14中std::move能被实现得更简便一些。多亏了函数返回值类型推导(看Item 3)以及标准库的别名模板std::remove_reference_t(看Item 9),std::move能被写成这样:

template<typename T>
decltype(auto) move(T&& param)
{
using ReturnType = remove_reference_t<T>&&;
return static_cast<ReturnType>(param);
}

看上去更简单了,不是吗?

因为std::move值只转换它的参数为右值,这里有一些更好的名字,比如说rvalue_cast。尽管如此,我们仍然使用std::move作为它的名字,所以记住std::move做了什么和没做什么很重要。它做的是转换,没有做move。

当然了,右值是move的候选人,所以把std::move应用在对象上能告诉编译器,这个对象是有资格被move的。这也就是为什么std::move有这样的名字:能让指定的对象更容易被move。

事实上,右值是move的唯一候选人。假设你写了一个代表注释的类。这个类的构造函数有一个std::string的参数,并且它拷贝参数到一个数据成员中。根据Item 41中的信息,你声明一个传值的参数:

class Annotation {
public:
explicit Annotation(std::string text); // 要被拷贝的参数
// 根据Item 41,声明为传值的
...
};

但是Annotation的构造函数只需要读取text的值。它不需要修改它。为了符合历史传统(把const用在任何可以使用的地方),你修改了你的声明,因此text成为了const的:

class Annotation {
public:
explicit Annotation(const std::string text)
...
};

为了在拷贝text到数据成员的时候不把时间浪费在拷贝操作上,你保持Item 41的建议并且把std::move用在text上,因此产生了一个右值:

class Annotation {
public:
explicit Annotation(const std::string text)
:value(std::move(text)) // “move” text到value中去;这段代码
{...} //做的事情不像看上去那样 ... private:
std::string value;
};

代码能够编译。代码能够链接。代码能够执行。代码把数据成员value的值设为text的内容。这段代码同完美的代码(你所要的版本)之间的唯一不同之处就是text不是被move到value中去的,它是拷贝过去的。当热,text通过std::move转换成了一个右值,但是text被声明为一个const std::string,所以在转换之前,text是一个左值const std::string,然后转换的结果就是一个右值const std::string,但是一直到最后,const属性保留下来了。

考虑一下const对于编译器决定调用哪个std::string构造函数有什么影响。这里有两种可能:

class string {						// std::string实际上是
public: // std::basic_string<char>的一个typedef
...
string(const string& rhs); // 拷贝构造函数
string(string& rhs); // move构造函数
...
};

在Annotation的构造函数的成员初始化列表中,std::move(text)的结果是一个const std::string的右值。这个右值不能传给std::string的move构造函数,因为move构造函数只接受非const std::string的右值引用。但是,这个右值能被传给拷贝构造函数,因为一个lvalue-reference-to-const(引用const的左值)能被绑定到一个const右值上去。因此即使text已经被转化成了一个右值,成员初始化列表还是调用了std::string中的拷贝构造函数。这样的行为本质上是为了维持const的正确性。一般把一个值move出去就相当于改动了这个对象,所以C++不允许const对象被传给一个能改变其自身的函数(比如move构造函数)。

我们从这个例子中得到两个教训。第一,如果你想要让一个对象能被move,就不要把这个对象声明为const。在const对象上的move请求会被默认地转换成拷贝操作。第二,std::move事实上没有move任何东西,它甚至不能保证它转换出来的对象能有资格被move。你唯一能知道的事情就是,把std::move用在一个对象之后,它变成了一个右值。

std::forward的情况和std::move相类似,但是std::move是无条件地把它的参数转换成右值的,而std::forward只在确定条件下才这么做。std::forward是一个有条件的转换。为了理解它什么时候转换,什么时候不转换,回忆一下std::forward是怎么使用的。最常见的情况就是,一个带universal引用的参数被传给另外一个参数:

void process(const Widget& lvalArg);			// 参数为左值
void process(Widget&& rvalArg); // 参数为右值 template<typename T> // 把参数传给process
void logAndProcess(T&& param) // 的模板
{
auto now =
std::chrono::system_clock::now(); // 取得正确的时间 makeLogEntry("Calling 'process'", now);
process(std::forward<T>(param));
}

考虑一下两个logAndProcess调用,一个使用左值,另外一个使用右值:

Widget w;

logAndProcess(w);				// 用左值调用
logAndProcess(std::move(w)); // 用右值调用

在logAndProcess内部,参数param被传给process函数。process重载了左值和右值两个版本。当我们用左值调用logAndProcess的时候,我们自然是希望这个左值作为一个左值被转发给process,然后当我们使用右值调用logAndProcess时,我们希望右值版本的process被调用。

但是param就和所有的函数参数一样,是一个左值。因此在logAndProcess内部总是调用左值版本的process。为了防止这样的事情发生,我们需要一种机制来让param在它被一个右值初始化(传给logAndProcess的参数)的时候转换成右值。这正好就是std::forward做的事情。这也就是为什么std::forward是一个条件转换:它只把用右值初始化的参数转换成右值。

你可能会奇怪std::forward怎么知道他的参数是不是用右值初始化的。举个例子吧,在上面的代码中,std::forward怎么会知道param是被左值还是右值初始化的呢?简单来说就是这个信息被包含在logAndProcess的模板参数T中了。这个参数被传给了std::forward,这样就让std::forward得知了这个信息。它具体怎么工作的细节请参考Item 28。

考虑到std::move和std::forward都被归结为转换,不同之处就是std::move总是执行转换,但是std::forward只在有些情况下执行转换,你可能会问我们是不是可以去掉std::move并且在所有的地方都只使用std::forward。从技术的角度来看,回答是可以:std::forward能做到所有的事情。std::move不是必须的。当然,这两个函数函数都不是“必须的”,因为我们能在使用的地方写cast,但是我希望我们能同意它们是必须的函数,好吧,真是令人心烦的事。

std::move的优点是方便,减少相似的错误,并且更加清晰。考虑一个类,对于这个类我们想要记录它的move构造函数被调用了多少次。一个能在move构造的时候自增的static计数器就是我们需要的东西了。假设这个类中唯一的非static数据是一个std::string,这里给出通常的办法(也就是使用std::move)来实现move构造函数:

class Widget {
public:
Widget(Widget&& rhs)
: s(std::move(rhs.s))
{ ++moveCtorCalls;}
} ... private: static std::size_t moveCtorCalls;
std::string s;
};

为了用std::forward来实现相同的行为,代码看起来像是这样的:

class Widget {
public:
Widget(Wdiget&& rhs) //不常见,以及不受欢迎的实现
: s(std::forward<std::string>(rhs.s))
//译注:为什么是std::string请看Item 1,用右值传入std::string&& str的话
//推导的结果T就是std::string,用左值传入,则推导的结果T会是std::string&
//然后这个T就需要拿来用作forward的模板类型参数了。
//详细的解释可以参考Item28
{ ++moveCtorCalls; }
};

首先注意std::move只需要一个函数参数(rhs.s),而std::forward却需要一个函数参数(rhs.s)以及一个模板类型参数(std::string)。然后注意一下我们传给std::forward的类型应该是一个非引用类型,因为我们约定好传入右值的时候要这么编码(传入一个非引用类型,看Item 28)。也就是说,这意味着std::move需要输入的东西比std::forward更少,还有,它去掉了我们传入的参数是右值时的麻烦(记住类型参数的编码)。它也消除了我们传入错误类型(比如,std::string&,这会导致数据成员用拷贝构造函数来替换move构造函数)的可能。

更加重要的是,使用std::move表示无条件转换到一个右值,然后使用std::forward表示只有引用的是右值时才转换到右值。这是两种非常不同的行为。第一个常常执行move操作,但是第二个只是传递(转发)一个对象给另外一个函数并且保留它原始的左值属性或右值属性。因为这些行为如此地不同,所以我们使用两个函数(以及函数名)来区分它们是很好的主意。

你要记住的事
  • std::move执行到右值的无条件转换。就其本身而言,它没有move任何东西。
  • std::forward只有在它的参数绑定到一个右值上的时候,它才转换它的参数到一个右值。
  • std::move和std::forward在运行期都没有做任何事情。

item 23: 理解std::move和std::forward的更多相关文章

  1. C++11 std::move和std::forward

    下文先从C++11引入的几个规则,如引用折叠.右值引用的特殊类型推断规则.static_cast的扩展功能说起,然后通过例子解析std::move和std::forward的推导解析过程,说明std: ...

  2. C++11中std::move、std::forward、左右值引用、移动构造函数的测试

    关于C++11新特性之std::move.std::forward.左右值引用网上资料已经很多了,我主要针对测试性能做一个测试,梳理一下这些逻辑,首先,左值比较熟悉,右值就是临时变量,意味着使用一次就 ...

  3. 透彻理解C++11新特性:右值引用、std::move、std::forward

    目录 浅拷贝.深拷贝 左值.右值 右值引用类型 强转右值 std::move 重新审视右值引用 右值引用类型和右值的关系 函数参数传递 函数返还值传递 万能引用 引用折叠 完美转发 std::forw ...

  4. 关于C++11中的std::move和std::forward

    std::move是一个用于提示优化的函数,过去的c++98中,由于无法将作为右值的临时变量从左值当中区别出来,所以程序运行时有大量临时变量白白的创建后又立刻销毁,其中又尤其是返回字符串std::st ...

  5. [C/C++]关于C++11中的std::move和std::forward

    http://www.cnblogs.com/cbscan/archive/2012/01/10/2318482.html http://blog.csdn.net/fcryuuhou/article ...

  6. std::move()和std::forward()

    std::move(t)负责将t的类型转换为右值引用,这种功能很有用,可以用在swap中,也可以用来解决完美转发. std::move()的源码如下 template<class _Ty> ...

  7. C++0x,std::move和std::forward解析

    1.std::move 1.1std::move是如何定义的 template<typename _Tp> constexpr typename std::remove_reference ...

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

    本文翻译自<effective modern C++>,由于水平有限,故无法保证翻译完全正确,欢迎指出错误.谢谢! 博客已经迁移到这里啦 右值引用只能绑定那些有资格被move的对象上去.如 ...

  9. C++ 11 左值,右值,左值引用,右值引用,std::move, std::foward

    这篇文章要介绍的内容和标题一致,关于C++ 11中的这几个特性网上介绍的文章很多,看了一些之后想把几个比较关键的点总结记录一下,文章比较长.给出了很多代码示例,都是编译运行测试过的,希望能用这些帮助理 ...

随机推荐

  1. 2018-05-09 5分钟入门CTS-尝鲜中文版TypeScript

    知乎原链 本文为中文代码示例之5分钟入门TypeScript的CTS版本. CTS作者是@htwx(github). 它实现了关键词和标准库的所有命名汉化. 本文并未使用附带的vscode相关插件(包 ...

  2. 苹果手机如何投屏到win10电脑上

    苹果手机中的IOS系统比安卓系统的确好用.苹果手机使用多久都不会出现手机卡顿的现象,一如既往的流畅自如,这就是人们追求苹果机的原因之一.苹果手机朋友们可能会觉得手机屏幕太小影响视觉怎么办,苹果手机如何 ...

  3. 唯一索引的一种使用情景【有则U无则I】

    这个知识点是最近一位面试老师问我的,当时对这种方法不了解,所以只能说那个中效率低的方法了,也就是先进性select判断,然后在执行更新或者插入操作,显然这种是很麻烦的,也自我反思一下,确实有很多的知识 ...

  4. Android内存优化(五) Lint代码扫描工具

     1.使用 工具栏 -> Analyze -> Inspect Code… 点击 Inspect Code 后会弹出检查范围的对话框: 默认是检查整个项目,我们可以点击 Custom sc ...

  5. https遇到自签名证书/信任证书

    对于CA机构颁发的证书Okhttp默认支持 可以直接访问 但是对于自定义的证书就不可以了(如:https ://kyfw.12306.cn/otn/), 需要加入Trust 下面分两部分来写,一是信任 ...

  6. vue缓存页面【二】

    keep-alive是vue内置的一个组件,可以使被它包含的组件处于保留状态,或避免被重新渲染. 用法:运行结果描述:input输入框内,路由切换输入框内部的内容不会发生改变.在keep-alive标 ...

  7. 【JS基础】类型转换——不同数据类型比较

    小试牛刀 输出下列数据比较结果 [] == 0; //==============================================================true [] == ...

  8. 华为ENSP进行evn实验,尚不完整,但已经有RT1、RT2、RT3、RT4了

    组网R1 -- CE12800  -- CE12800 --- R2 其中R1.R2是模拟VM的 R1的配置: interface GigabitEthernet0/0/0#interface Gig ...

  9. 基于Python3的漏洞检测工具 ( Python3 插件式框架 )

    目录 Python3 漏洞检测工具 -- lance screenshot requirements 关键代码 usage documents Any advice or sugggestions P ...

  10. 探索SQL Server元数据(二)

    背景 上一篇中,我介绍了SQL Server 允许访问数据库的元数据,为什么有元数据,如何使用元数据.这一篇中我会介绍如何进一步找到各种有价值的信息.以触发器为例,因为它们往往一起很多问题. 那么如何 ...