[C++中级进阶]001_C++0x里的完美转发到底是神马?
[C++中级进阶]001_C++0x里的完美转发到底是神马?
转载至:http://www.cnblogs.com/alephsoul-alephsoul/archive/2013/01/10/2853900.html
问题描述
C++无疑是十分强大的,但是你可知道,在C++0x标准出现之前,在C++界里有一个十分棘手而未能解决的问题——参数转发。问题的描述如下:
对于一个给定的函数E(a1, a2, ..., an),它有参数a1, a2, ..., an,你不可能写出一个函数F(a1, a2, ..., an),使得该函数与E(a1, a2, ..., an)完全等价。
对这个问题进而拆分,它有两点:第一,函数F(a1, a2, ..., an)比如能够接受任意的参数列表,并在不改变参数性质的前提下,将参数传递给E(a1, a2, ..., an);第二,函数F(a1, a2, ..., an)必须能够将函数E(a1, a2, ..., an)的结果返回给自己的调用者。
本文就第一点的参数转发进行讲解。
标准解决方案的三大规则
从更为严密的逻辑角度上来考虑转发的问题,我们的转发实现需要满足下面三个条件。这里假设函数F(a1, a2, ..., an)调用函数G(a1, a2, ..., an)。
C1. 对于能使用函数F(a1, a2, ..., an)的地方,函数G(a1, a2, ..., an)也一定能使用。
C2. 对于不能使用函数F(a1, a2, ..., an)的地方,函数G(a1, a2, ..., an)也一定不能使用。
C3. 实现函数F(a1, a2, ..., an)的时候,复杂度必须是线性增加的。(这一点乍看起来可能不理解,现在可以不用理解,后面有实例说明)
七种转发实现方案
在实现转发的方案里,有七种需要我们了解,本文会为你一一介绍,这七种转发里,只有第七种能实现完美转发。
七种转发中,1-4不需要对C++0x以前的标准进行修改,第五种转发需要对标准关于参数推倒的规则进行修改,第六种和第七种都用到了C++0x标准里的右值引用。
方案1. 非常量左值引用转发
何谓非常量左值引用呢?形如int& a;我们就把a叫做非常量左值引用。这里的左值引用就是我们平时使用的&。
这个解决方案的实例代码如下:
![](https://common.cnblogs.com/images/copycode.gif)
1 template<class A1, class A2, class A3>
2 void f(A1 & a1, A2 & a2, A3 & a3)
3 {
4 return g(a1, a2, a3);
5 }
6
7 void g(int a1, int a2, int a3){
8
9 }
![](https://common.cnblogs.com/images/copycode.gif)
转发失败原因:这个转发不能传入非常量右值,即下面的代码是无法通过编译的。
1 int main()
2 {
3 f(1, 2, 3);
4 }
但是这种解决方案并不是一无是处的,对于那些只可能传入左值的场景来说,比如Boost库中的Iterator,这种应用还是能够看到的。只是它并不是一个完美的转发方案。
方案2. 常量左值引用转发
何谓常量左值引用呢?形如const int& a;我们就把a叫做常量左值引用。
这个解决方案的实例代码如下:
![](https://common.cnblogs.com/images/copycode.gif)
1 template<class A1, class A2, class A3>
2 void f(A1 const & a1, A2 const & a2, A3 const & a3)
3 {
4 return g(a1, a2, a3);
5 }
6
7 void g(int& a1, int& a2, int& a3){
8
9 }
![](https://common.cnblogs.com/images/copycode.gif)
转发失败原因:上面的函数f虽然可以接受任何参数列表,但是当函数g接受非常量左值引用变量时,函数f是无法将常量左值引用参数传递给非常量左值引用的。
这个解决方案一般用于拷贝构造函数,因为拷贝构造函数一般传递的都是形如const A&的参数,但也不排除有的拷贝构造函数比较变态,不传递const A&的参数。
方案3. 非常量左值引用+常量左值引用转发
这个解决方案的实例代码如下:
![](https://common.cnblogs.com/images/copycode.gif)
1 template<class A1>
2 void f(A1 & a1)
3 {
4 return g(a1);
5 }
6
7 template<class A1>
8 void f(A1 const & a1)
9 {
10 return g(a1);
11 }
![](https://common.cnblogs.com/images/copycode.gif)
转发失败原因:上面的实现的确可以满足所有的参数都能传递了,并且当函数g接受非常量参数时,编译器也能找到最佳的匹配模板函数,即第一个。当然这个前提是所有的编译器达成共识,认定第一个模板函数。除此之外,还有一个重要的问题,当函数的参数有三个的时候,我们不得不像下面这样来实现我们的函数f:
![](https://common.cnblogs.com/images/copycode.gif)
1 template<class A1, class A2, class A3>
2 void f(A1 const & a1, A2 const & a2, A3 const & a3)
3 {
4 return g(a1, a2, a3);
5 }
6
7 template<class A1, class A2, class A3>
8 void f(A1 & a1, A2 const & a2, A3 const & a3)
9 {
10 return g(a1, a2, a3);
11 }
12
13 template<class A1, class A2, class A3>
14 void f(A1 const & a1, A2 & a2, A3 const & a3)
15 {
16 return g(a1, a2, a3);
17 }
18
19 template<class A1, class A2, class A3>
20 void f(A1 & a1, A2 & a2, A3 const & a3)
21 {
22 return g(a1, a2, a3);
23 }
24
25 template<class A1, class A2, class A3>
26 void f(A1 const & a1, A2 const & a2, A3 & a3)
27 {
28 return g(a1, a2, a3);
29 }
30
31 template<class A1, class A2, class A3>
32 void f(A1 & a1, A2 const & a2, A3 & a3)
33 {
34 return g(a1, a2, a3);
35 }
36
37 template<class A1, class A2, class A3>
38 void f(A1 const & a1, A2 & a2, A3 & a3)
39 {
40 return g(a1, a2, a3);
41 }
42
43 template<class A1, class A2, class A3>
44 void f(A1 & a1, A2 & a2, A3 & a3)
45 {
46 return g(a1, a2, a3);
47 }
![](https://common.cnblogs.com/images/copycode.gif)
是的,这是指数级别的增长,这个就不符合我们的C3规则了,呵呵,这下理解C3规则是什么含义了吧。
方案4. 常量左值引用+const_cast转发
const_cast的作用是什么呢?它可以去除常量的的const属性,这个转换可以解决方案2里遇到的问题。
这个解决方案的实例代码如下:
1 template<class A1, class A2, class A3>
2 void f(A1 const & a1, A2 const & a2, A3 const & a3)
3 {
4 return g(const_cast<A1 &>(a1), const_cast<A2 &>(a2), const_cast<A3 &>(a3));
5 }
转发失败原因:很显然,去除了const属性,我们就能修改原来的常量了,这样的转发会造成对常量的修改。这里让我十分郁闷的是,C++既然提供了const,还非得提供一个const_cast,这是矛盾的存在。或许这就是C++的牛逼之处吧,哈哈。
方案5. 非常量左值引用+修改的参数推倒规则转发
这里说明一下,我在看胡健的博客的时候,对“修改的参数推倒”这句话费透了脑筋。乍一看不懂,仔细看还是不懂,基础不扎实可能吧。不过最后还是搞懂了,这里所谓的“修改参数推倒”是指修改C++的现有标准。在模板编程里,有一种参数推倒的说法。当你传递int类型的参数时,编译器会为你找到最佳匹配的模板函数,然后再把参数传递给这个模板函数。在方案1里,导致我们失败的事情就是无法传递非常量右值,但是如果修改C++标准,我们就能够将非常量右值推倒成常量右值。
但是,修改标准后,对于现有用C++实现的代码造成十分巨大的破坏。具体参照如下代码:
![](https://common.cnblogs.com/images/copycode.gif)
1 template<class A1>
2 void f(A1 & a1)
3 {
4 std::cout << 1 << std::endl;
5 }
6
7 void f(long const &)
8 {
9 std::cout << 2 << std::endl;
10 }
11
12 int main()
13 {
14 f(5); // 在既有参数推倒规则,会打印2;修改参数推倒规则后,会打印1
15 int const n(5);
16 f(n); // 这种情况好一点,都会打印1
17 }
![](https://common.cnblogs.com/images/copycode.gif)
看出来对现有代码的破坏了吗?注意注释。
方案6. 右值引用转发
何谓右值引用呢?这是C++0x里新追加的特性,如果想更清楚一点,可以参考胡健的博客。
这个解决方案的实例代码如下:
1 template<class A1, class A2, class A3>
2 void f(A1 && a1, A2 && a2, A3 && a3)
3 {
4 return g(a1, a2, a3);
5 }
转发失败原因:函数g无法接收左值,因为不能将一个左值传递给一个右值引用。另外,当传递非常量右值时也会存在问题,因为此时a1、a2、a3本身是左值,这样当F的参数是非常量左值引用时,我们就可以来修改传入的非常量右值了,而右值是不能被修改的。
完美方案7. 右值引用+修改的参数推倒规则转发
你可能疑惑,不是说过修改参数推倒规则后会导致对既有代码的破坏吗?是的,不过那是对左值参数推倒规则的修改,我们这里要修改的是针对右值引用推倒规则的修改。首先,要理解参数推倒规则,我们要理解引用叠加规则:
1、T& + & = T&
2、T& + && = T&
3、T&& + & = T&
4、T或T&& + && = T&&
如何验证上面的引用叠加规则呢?我们可以用下面这样一段代码来验证这个问题:
![](https://common.cnblogs.com/images/copycode.gif)
1 #include <iostream>
2 using namespace std;
3
4 typedef int& LRINT;
5 typedef int&& RRINT;
6
7 int main(){
8
9 int a = 10;
10
11 // 左值引用
12 LRINT b = a; // 单纯:&
13 LRINT& c = a; // 叠加:& + & 不能写做:LRINT& c = 10; 可见c是左值引用
14
15 // 右值引用
16 RRINT d = 10; // 单纯:&&
17 RRINT&& e = 10; // 叠加:&& + && 不能写做:RRINT&& e = a; 可见e是右值引用
18 LRINT&& f = a; // 叠加:& + && 不能写做:LRINT&& f = 10; 可见f是左值引用
19 RRINT& g = a; // 叠加:&& + & 不能写做:RRINT& g = 10; 可见g是左值引用
20
21 system("pause");
22 return 0;
23 }
![](https://common.cnblogs.com/images/copycode.gif)
理解了引用叠加规则后,让我们来看看修改后的针对右值引用的参数推倒规则:
修改后的针对右值引用的参数推导规则为:若函数模板的模板参数为A,模板函数的形参为A&&,则可分为两种情况讨论:
1、若实参为T&,则模板参数A应被推导为引用类型T&。(由引用叠加规则第2点T& + && = T&和A&&=T&,可得出A=T&)
2、若实参为T&&,则模板参数A应被推导为非引用类型T。(由引用叠加规则第4点T或T&& + && = T&&和A&&=T&&,可得出A=T或T&&,强制规定A=T)
应用了新的参数推导规则后,我们来看下面的代码:
1 template<class A1>
2 void f(A1 && a1)
3 {
4 return g(static_cast<A1 &&>(a1));
5 }
当传给f一个左值(类型为T)时,由于模板是一个引用类型,因此它被隐式装换为左值引用类型T&,根据推导规则1,模板参数A被推导为T&。这样,在f内部调用F(static_cast<A &&>(a))时,static_cast<A &&>(a)等同于static_cast<T& &&>(a),根据引用叠加规则第2点,即为static_cast<T&>(a),这样转发给g的还是一个左值。
当传给f一个右值(类型为T)时,由于模板是一个引用类型,因此它被隐式装换为右值引用类型T&&,根据推导规则2,模板参数A被推导为T。这样,在G内部调用F(static_cast<A &&>(a))时,static_cast<A &&>(a)等同于static_cast<T&&>(a),这样转发给F的还是一个右值(不具名右值引用是右值)。
可见,使用该方案后,左值和右值都能正确地进行转发,并且不会带来其他问题。
参考资料
1.【原】C++ 11完美转发 http://www.cnblogs.com/hujian/archive/2012/02/17/2355207.html
2.【原】C++ 11右值引用 http://www.cnblogs.com/hujian/archive/2012/02/13/2348621.html
3. The Forwarding Problem: Arguments http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1385.htm
4. A Proposal to Add an Rvalue Reference to the C++ Language http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1690.html
5. A Brief Introduction to Rvalue References http://www.artima.com/cppsource/rvalue.html
[C++中级进阶]001_C++0x里的完美转发到底是神马?的更多相关文章
- 无需Flash录视频——HTML5中级进阶
前言 HTML5的权限越来越大了,浏览器可以直接调用摄像头.麦克风了,好激动啊.我们要用纯洁的HTML代码造出自己的天地. 视频采集 本篇介绍的栗子 都是在chrome 47 版本以上的,低版本的可能 ...
- C++ 左值与右值 右值引用 引用折叠 => 完美转发
左值与右值 什么是左值?什么是右值? 在C++里没有明确定义.看了几个版本,有名字的是左值,没名字的是右值.能被&取地址的是左值,不能被&取地址的是右值.而且左值与右值可以发生转换. ...
- 完美转发(perfect forwarding)、universal reference、引用折叠(collasping)
首先要分清: C++里的值只有两种值:左值.右值.—— 其本质应该是内存中存储的值/instance分两种:一种是持久的,一种是“短暂的” 也只有两种引用: 左值引用.右值引用. ——引用,就是这个内 ...
- Effective Modern C++:05右值引用、移动语义和完美转发
移动语义使得编译器得以使用成本较低的移动操作,来代替成本较高的复制操作:完美转发使得人们可以撰写接收任意实参的函数模板,并将其转发到目标函数,目标函数会接收到与转发函数所接收到的完全相同的实参.右值引 ...
- C++11 左值引用和右值引用与引用折叠和完美转发
1.左值与右值 最感性的认识. 当然,左值也是可以在右边的. 左值是可以被修改的,右值不能. 当然取地址也是. 生存周期一般左值会比右值的长,一般右值都计算时产生的无名临时对象,存在时间比较短. 下面 ...
- C++11 变长模版和完美转发实例代码
C++11 变长模版和完美转发实例代码 #include <memory>#include <iostream>#include <vector>#include ...
- SQLSERVER 里经常看到的CACHE STORES是神马东东?
SQLSERVER 里经常看到的CACHE STORES是神马东东? 当我们在SSMS里执行下面的SQL语句清空SQLSERVER的缓存的时候,我们会在SQL ERRORLOG里看到一些信息 DBCC ...
- 翻译「C++ Rvalue References Explained」C++右值引用详解 Part8:Perfect Forwarding(完美转发):解决方案
本文为第八部分,目录请参阅概述部分:http://www.cnblogs.com/harrywong/p/cpp-rvalue-references-explained-introduction.ht ...
- 第16课 右值引用(3)_std::forward与完美转发
1. std::forward原型 template <typename T> T&& forward(typename std::remove_reference< ...
随机推荐
- Javascript学习笔记3 Javascript与BOM简介
什么是BOM BOM是browser object model的缩写,简称浏览器对象模型 BOM提供了独立于内容而与浏览器窗口进行交互的对象 由于BOM主要用于管理窗口与窗口之间的通讯,因此其核心对象 ...
- Python笔记总结week2
1. 关于Python程序执行原理:
- Java项目:学生成绩管理系统(二)
学生成绩管理系统(二):项目介绍 一.设计要求: 1.1 简单的图形界面登录功能. 1.2 对数据库的的信息的查询功能. 1.3 对数据库的的信息的修改功能. 1.4 对数据库的的信息的删除功能. 1 ...
- 听着好像很牛的特效——幽灵按钮DOM
给大家分享一个听着好像很牛的东西——幽灵按钮,这个玩意对于艺术设计细胞在高中决定不在考试试卷上画画的我来说,实在不感冒.但是这个按钮的设计元素很流行,一个网页东西不做几个,光放上几个按钮就会显得很高端 ...
- asp.net发邮件功能
protected void SendMail() { try { string CreaterName = ""; string examiner = ""; ...
- windows下登录lunix服务器
在微信项目中,负责发布,我就把我用到的记录一下.有两种登录方式,看你要做什么操作. 1.SecureCRT 支持命令行操作.(主要是发布程序) 调试.微信公众号规定要有服务器的网址,一般公司的都是内网 ...
- PHP redis 批量操作
参考网站 phpredis扩展 :https://github.com/phpredis/phpredis#pconnect-popen 命令参考:http://doc.redisfans.com ...
- Intellij Idea 编辑器使用之 安装、破解 版本15.0.1
工欲善其事必先利其器. 早就听说了Intellij idea 这个编辑器.也曾尝试过,由于对eclipse,myeclipse用的比较习惯.顺手了,所以对于Intellij idea 也只是浅尝辄止. ...
- [转]Bat脚本处理ftp超强案例解说
Bat脚本处理ftp超强案例解说 转自:http://369369.blog.51cto.com/319630/842341 前言: 公司有几百台windows服务器,每次程序更新,如果是一台 ...
- mydumper 快速高效备份mysql,按照表生成备份文件,快速恢复
Mydumper是一个针对MySQL和Drizzle的高性能多线程备份和恢复工具.开发人员主要来自MySQL,Facebook,SkySQL公司.目前已经在一些线上使用了Mydumper. Mydum ...