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

http://blog.sina.com.cn/s/blog_53b7ddf00101p5t0.html

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

比如:

1 std::string fileContent = “oldContent”;
2 s = readFileContent(fileName);

因为并不是所有情况下,C++编译器都能进行返回值优化,所以,向上面的例子中,往往会创建多个字符串。readFileContent如果没有内部状态,那么,它的返回值多半是std::string(const std::string的做法不再被推荐了),而不是const std::string&。这是一个浪费,函数的返回值被拷贝到s中后,栈上的临时对象就被销毁了。

在C++11中,编码者可以主动提示编译器,readFileContent返回的对象是临时的,可以被挪作他用:std::move。

将上面的例子改成:

1 std::string fileContent = “oldContent”;
2 s = std::move(readFileContent(fileName));

后,对象s在被赋值的时候,方法std::string::operator =(std::string&&)会被调用,符号&&告诉std::string类的编写者,传入的参数是一个临时对象,可以挪用其数据,于是std::string::operator =(std::string&&)的实现代码中,会置空形参,同时将原本保存在中形参中的数据移动到自身。

不光是临时变量,只要是你认为不再需要的数据,都可以考虑用std::move移动。

比较有名的std::move用法是在swap中:

复制代码
1 template
2 void swap(T& a, T& b)
3 {
4 T t(std::move(a)); // a为空,t占有a的初始数据
5 a = std::move(b); // b为空, a占有b的初始数据
6 b = std::move(t); // t为空,b占有a的初始数据
7 }
复制代码

总之,std::move是为性能而生的,正式因为了有了这个主动报告废弃物的设施,所以C++11中的STL性能大幅提升,即使C++用户仍然按找旧有的方式来编码,仍然能因中新版STL等标准库的强化中收益。

 

std::forward是用于模板编程中的,如果不需要编写通用的模板类和函数,可能不怎么用的上它。

要认识它的作用,需要知道C++中的几条规则:(这里有篇挺好的文章:http://blog.csdn.net/zwvista/article/details/6848582,但似乎因标准的更新,其中的规则已不完全成立了)

1. 引用折叠规则:

X& + & => X&
X&& + & => X&
X& + && => X&
X&& + && => X&&

2. 对于模板函数中的形参声明T&&(这里的模板参数T,最终推演的结果可能不是一个纯类型,它可能还会带有引用/常量修饰符,如,T推演为const int时,实际形参为const int &&),会有如下规则:

如果调用函数时的实参为U&(这里的U可能有const/volatile修饰,但没有左/右引用修饰了),那么T推演为U&,显然根据上面的引用折叠规则,U& &&=>U&。

如果调用实参为U&&,虽然将T推导为U&&和U都能满足折叠规则(U&& &&=> U&&且U &&=>U&&),但标准规定,这里选择将T推演为U而非U&&。

总结一下第2条规则:当形参声明为T&&时,对于实参U&,T被推演为U&;当实参是U&&时,T被推演为U。当然,T和U具有相同的const/volatile属性。

3.这点很重要,也是上面zwvista的文章中没有提到的:形参T&& t中的变量t,始终是左值引用,即使调用函数的实参是右值引用也不例外。可以这么理解,本来,左值和右值概念的本质区别就是,左值是用户显示声明或分配内存的变量,能够直接用变量名访问,而右值主要是临时变量。当一个临时变量传入形参为T&& t的模板函数时,T被推演为U,参数t所引用的临时变量因为开始能够被据名访问了,所以它变成了左值。这也就是std::forward存在的原因!当你以为实参是右值所以t也应该是右值时,它跟你开了个玩笑,它是左值!如果你要进一步调用的函数会根据左右值引用性来进行不同操作,那么你在将t传给其他函数时,应该先用std::forward恢复t的本来引用性,恢复的依据是模板参数T的推演结果。虽然t的右值引用行会退化,变成左值引用,但根据实参的左右引用性不同,T会被分别推演为U&和U,这就是依据!因此传给std::forward的两个参数一个都不能少:std::forward(t)。

 

再来,讨论一下,一个模板函数如果要保留参数的左右值引用性,为什么应该声明为T&&:

如果声明函数f(T t):实参会直接进行值传递,失去了引用性。

如果声明函数f(T &t): 根据引用折叠法则,无论T是U&还是U&&,T&的折叠结果都只会是U&,即,这个声明不能用于匹配右值引用实参。

如果声明函数f(T &&t): 如果T为U&,T&&的结果是U&,可以匹配左值实参;如果T为U&&,T&&的结果是U&&,可以匹配右值实参。又因为T的cv性和U相同,所以这种声明能够保留实参的类型信息。

 

先来看一组帮助类:

1 template struct TypeName { static const char *get(){ return "Type"; } };
2 template struct TypeName<<SPAN style="PADDING-BOTTOM: 0px; LINE-HEIGHT: 1.5; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New'; COLOR: rgb(0,0,255); FONT-SIZE: 12px; PADDING-TOP: 0px">const T> { static const char *get(){ return "const Type"; } };
3 template struct TypeName { static const char *get(){ return "Type&"; } };
4 template struct TypeName<<SPAN style="PADDING-BOTTOM: 0px; LINE-HEIGHT: 1.5; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New'; COLOR: rgb(0,0,255); FONT-SIZE: 12px; PADDING-TOP: 0px">const T&> { static const char *get(){ return "const Type&"; } };
5 template struct TypeName { static const char *get(){ return "Type&&"; } };
6 template struct TypeName<<SPAN style="PADDING-BOTTOM: 0px; LINE-HEIGHT: 1.5; MARGIN: 0px; PADDING-LEFT: 0px; PADDING-RIGHT: 0px; FONT-FAMILY: 'Courier New'; COLOR: rgb(0,0,255); FONT-SIZE: 12px; PADDING-TOP: 0px">const T&&> { static const char *get(){ return "const Type&&"; } };

在模板函数内部将模板参数T传给TypeName,就可以访问T的类型字符串:TypeName::get()。

 

再一个帮助函数,用于打印一个表达式的类型:

1 template
2 void printValType(T &&val)
3 {
4 cout << TypeName::get() << endl;
5 }

注意3条规则在这个模板函数上的应用。规则1,解释了T&& val的声明足以保留实参的类型信息。规则2,说明了,当实参是string&时,T就是string&;当实参是const string&&时,T就是const string(而非const string&&)。规则3,强调,无论实参是string&还是string&&,形参val的类型都是string&!

注意TypeName的写法,因为T只能为U&或者U,显然T&&可以根据折叠法则还原为实参类型U&和U&&。

 

这里是常见的const/左右引用组合的情形:

1 class A{}; // 测试类
2 A& lRefA() { static A a; return a;} // 左值
3 const A& clRefA() { static A a; return a;} // 常左值
4 A rRefA() { return A(); } // 右值
5 const A crRefA() { return A(); } // 常右值

测试一下上面的表达式类型:

1 printValType(lRefA());
2 printValType(clRefA());
3 printValType(rRefA());
4 printValType(crRefA());

输出依次是: Type&,const Type&,Type&&,const Type&&。

 

现在正式来探讨std::forward的实现。

回顾一下使用std::forward的原因:由于声明为f(T&& t)的模板函数的形参t会失去右值引用性质,所以在将t传给更深层函数前,可能会需要回复t的正确引用行,当然,修改t的引用性办不到,但根据t返回另一个引用还是可以的。恰好,上面的函数printValType是一个会根据实参类型不同,作出不同反映的函数,所以可以把它作为f的内层函数,来检测f有没有正确的修正t的引用行。

复制代码
 1 template
2 void f(T &&a)
3 {
4 printValType(a);
5 }
6
7 int main()
8 {
9 f(lRefA());
10 f(clRefA());
11 f(rRefA());
12 f(crRefA());
13 }
复制代码

输出:Type&,const Type&,Type&,const Type&。

可见后两个输出错了,这正是前面规则3描述的,当实参是右值引用时,虽然T被推演为U,但是参数a退化成了左值引用。

直接应用std::forward:

1 template
2 void f(T &&a)
3 {
4 printValType(std::forward(a));
5 }

输出:Type&,const Type&,Type&&,const Type&&。

输出正确了,这就是std::forward的作用啊。如果更深层的函数也需要完整的引用信息,如这里的printValType,那就应该在传递形参前先std::forward!

 

在编写自己的forward函数之前,先来尝试直接强制转化参数a:

1 template
2 void f(T &&a)
3 {
4 printValType((T&&)a);
5 }

输出:Type&,const Type&,Type&&,const Type&&。

正确!因为不管T被推演为U&还是U,只要T&&肯定能还原为U&和U&&。

 

考虑下自己的forward函数应该怎么写:

 因为在forward的调用方中,形参已经丢失了右值引用信息,唯一的参考依据是T,要根据T还原为正确的参数,得T&&,因此,强制转换和返回类型都是T&&了,当然,forward还必须被以forward()的方式显示指定模板类型,这样才能保证forward的模板参数T和上层函数f的T是相同类型。首先:

1 template
2 T&& forward(... a)
3 {
4 return (T&&)a;
5 }

调用方f一定得显示指定类型forward。

形参怎么写?形参a的类型由T构成,而且forward的实参一定是左值(暂时不考虑forward(std::string())的使用方法),也就是说,无论T是U&还是U,形参a的类型一定都得是U&,才能和实参匹配,所以,结果是:

1 template
2 T&& forward(T& a)
3 {
4 return (T&&)a;
5 }

测试,输出:Type&,const Type&,Type&&,const Type&&。
正确!

 

再试下,如果f调用forward的时候,使用forward(a)的方式,没有显示指定模板类型会怎么样:

1 template
2 void f(T &&a)
3 {
4 printValType(forward(a));
5 }

输出:T&&,const Type&&,Type&&,const Type&&。

错了。分析下,因为实参始终是左值,所以forward的形参T& a中,T就被推演为U,因此(T&&)a也就是(U&&)a所以结果错误。

为了避免用户使用forward(a),因此应该禁用forward的自动模板参数推演功能!可以借助std::identity,另外,将(T&&)换成static_cast,规范一下:

1 template
2 T&& forward(typename std::identity::type& a)
3 {
4 return static_cast(a);
5 }

 

上面讲的是针对T为U&或U,而实参始终为左值的情况,这是常见的情形;不过也有实参为右值的情况,还需要改进上面这个forward,但我这里就不写了。

这是我手里的gcc4.5.2的forward实现:

复制代码
 1   /// forward (as per N2835)
2 /// Forward lvalues as rvalues.
3 template
4 inline typename enable_if::value, _Tp&&>::type
5 forward(typename std::identity<_Tp>::type& __t)
6 { return static_cast<_Tp&&>(__t); }
7
8 /// Forward rvalues as rvalues.
9 template
10 inline typename enable_if::value, _Tp&&>::type
11 forward(typename std::identity<_Tp>::type&& __t)
12 { return static_cast<_Tp&&>(__t); }
13
14 // Forward lvalues as lvalues.
15 template
16 inline typename enable_if::value, _Tp>::type
17 forward(typename std::identity<_Tp>::type __t)
18 { return __t; }
19
20 // Prevent forwarding rvalues as const lvalues.
21 template
22 inline typename enable_if::value, _Tp>::type
23 forward(typename std::remove_reference<_Tp>::type&& __t) = delete;
复制代码

第1/3版本就相当于我之前的实现,而版本2/4是实参为右值的情况,至于后者这种取舍的原因,还得去自己研究下使用场合和文档了。

我手里的vc2010实现的forward和我之前的实现相同,显然还不够,不过vc2010本来对标准也就还支持得少..


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/383860.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux I/O复用之select函数详解

http://blog.csdn.net/y396397735/article/details/55004775 select函数的功能和调用顺序 使用select函数时统一监视多个文件描述符的&#xff1a; 1、 是否存在套接字接收数据&#xff1f; 2、 无需阻塞传输数据的套接字有哪些? 3、 哪些套接字发生了异常&#xff1f; sel…

深入研究socket编程(3)——使用select函数编写客户端和服务器

http://blog.csdn.net/chenxun_2010/article/details/50488394 首先看原先《UNIX网络编程——并发服务器&#xff08;TCP&#xff09;》的代码&#xff0c;服务器代码serv.c&#xff1a; [cpp] view plaincopy #include<stdio.h> #include<sys/types.h> #inclu…

Ubuntu安装搭建Clion环境

呜呜呜&#xff0c;太辛苦了&#xff0c;我终于安装好这个了。 大概过程就是先在官网下载安装包&#xff0c;然后解压以后用终端移动到对应文件夹下运行clin.sh 运行完以后会有一些窗口&#xff0c;第一个选择don’t~~&#xff0c;然后点击ok 接受&#xff08;你可以不接受…

UNIX网络编程——select函数的并发限制和 poll 函数应用举例

http://blog.csdn.net/chenxun_2010/article/details/50489577 一、用select实现的并发服务器&#xff0c;能达到的并发数&#xff0c;受两方面限制 1、一个进程能打开的最大文件描述符限制。这可以通过调整内核参数。可以通过ulimit -n来调整或者使用setrlimit函数设置&#x…

【Java学习笔记二】继承和多态

与C不同的是&#xff0c;在Java中&#xff0c;一个类只能直接继承另一个类&#xff0c;而不允许继承多个类&#xff0c;这个新类称为继承类、派生类或者子类&#xff0c;而被继承的类称为基类或者父类。 继承类能够继承基类的群不属性和行为。 面向对象程序设计的三大特点为&…

Linux系统编程——线程池

http://blog.csdn.net/tennysonsky/article/details/46490099# 线程池基本原理 在传统服务器结构中&#xff0c;常是有一个总的监听线程监听有没有新的用户连接服务器&#xff0c;每当有一个新的用户进入&#xff0c;服务器就开启一个新的线程用户处理这 个用户的数据包。这个线…

简单Linux C线程池

http://www.cnblogs.com/venow/archive/2012/11/22/2779667.html 大多数的网络服务器&#xff0c;包括Web服务器都具有一个特点&#xff0c;就是单位时间内必须处理数目巨大的连接请求&#xff0c;但是处理时间却是比较短的。在传统的多线程服务器模型中是这样实现的&#xff1…

IO多路复用之poll总结

http://www.cnblogs.com/Anker/p/3261006.html 1、基本知识 poll的机制与select类似&#xff0c;与select在本质上没有多大差别&#xff0c;管理多个描述符也是进行轮询&#xff0c;根据描述符的状态进行处理&#xff0c;但是poll没有最大文件描述符数量的限制。poll和select同…

【C++学习笔记二】C++继承

继承 继承允许我们一句另一个类来定义一个类&#xff0c;这使得继承和维护一个程序变得更加容易&#xff0c;也达到了重用代码功能和提高执行效率的效果。 一般格式为&#xff1a; class 派生类名 :访问修饰符 基类名{};其中访问修饰符是public protected private中的一个&a…

处理大并发之二 对epoll的理解,epoll客户端服务端代码

http://blog.csdn.net/wzjking0929/article/details/51838370 序言&#xff1a; 该博客是一系列的博客&#xff0c;首先从最基础的epoll说起&#xff0c;然后研究libevent源码及使用方法&#xff0c;最后研究nginx和node.js&#xff0c;关于select,poll这里不做说明&#xff0c…

C++基类指针指向派生类(指针)

我们常用基类指针指向派生类对象来实现多态性。 私有继承不允许基类指针指向派生类 基类指针只能访问到基类中含有的公有成员。 当用基类指针指向派生类对象在动态分配堆上内存的时候&#xff0c;析构函数必须是虚函数! 成员如果是数据成员的话访问的是基类的版本&#xff…

一个简单的linux线程池

http://blog.csdn.net/wzjking0929/article/details/20312675 线程池&#xff1a;简单地说&#xff0c;线程池 就是预先创建好一批线程&#xff0c;方便、快速地处理收到的业务。比起传统的到来一个任务&#xff0c;即时创建一个线程来处理&#xff0c;节省了线程的创建和回收的…

C++制表符

制表符的转义字符为\t&#xff0c;一般情况下长度为8个空格&#xff0c;这里的8个指的是从上一个字符串的开头开始算&#xff0c;往后数8个&#xff0c;不够的话就补空格。 如果前面的字符串的长度大于等于8个&#xff0c;例如前面字符串的长度为x,那么就会补(8-x%8)个空格 例…

C++派生类含有成员对象构造函数析构函数顺序

参考博客&#xff1a;传送门1 当类中含有对象成员时&#xff1a; 类的构造函数要包含对成员对象的初始化&#xff0c;如果构造函数的成员初始化列表没有包含对成员对象的初始化&#xff0c;系统会自动调用成员对象的无参构造函数。顺序上&#xff1a;先调用成员对象的构造函数…

链表逆序的原理及实例

http://blog.csdn.net/wangqing_12345/article/details/51757294 尾插法建立链表&#xff0c;带头结点设链表节点为typedef struct node {int data;struct node *next;}node_t, *pnode_t;要求将一带链表头List head的单向链表逆序。 分析&#xff1a; 1). 若链表为空或只有一个…

C++关于虚基类、构造函数、析构函数、成员对象的两个程序浅析

预备博客&#xff1a; C虚继承中构造函数和析构函数顺序问题以及原理 C派生类含有成员对象构造函数析构函数顺序 C虚基类成员可见性 程序一如下&#xff1a; #include<iostream> using namespace std; class A { public:A(int a) :x(a) { cout << "A const…

C++小型公司管理系统

项目要求&#xff1a; 编写一个程序实现小型公司的人员信息管理系统。该公司雇员&#xff08;employee&#xff09;包括经理&#xff08;manager&#xff09;&#xff0c;技术人员&#xff08;technician&#xff09;、销售员&#xff08;salesman&#xff09;和销售部经理&…

Linux网络编程“惊群”问题总结

http://www.cnblogs.com/Anker/p/7071849.html 1、前言 我从事Linux系统下网络开发将近4年了&#xff0c;经常还是遇到一些问题&#xff0c;只是知其然而不知其所以然&#xff0c;有时候和其他人交流&#xff0c;搞得非常尴尬。如今计算机都是多核了&#xff0c;网络编程框架也…

yfan.qiu linux硬链接与软链接

http://www.cnblogs.com/yfanqiu/archive/2012/06/11/2545556.html Linux 系统中有软链接和硬链接两种特殊的“文件”。 软链接可以看作是Windows中的快捷方式&#xff0c;可以让你快速链接到目标档案或目录。 硬链接则透过文件系统的inode来产生新档名&#xff0c;而不是产生…

Linux C++线程池实例

http://www.cnblogs.com/danxi/p/6636095.html 想做一个多线程服务器测试程序&#xff0c;因此参考了github的一些实例&#xff0c;然后自己动手写了类似的代码来加深理解。 目前了解的线程池实现有2种思路&#xff1a; 第一种&#xff1a; 主进程创建一定数量的线程&#xff0…