Effective Modern C++ 条款1:理解模板型别推导
成百上千的程序员都在向函数模板传递实参,并拿到了完全满意的结果,而这些程序员中却有很多对这些函数使用的型别是如何被推导出的过程连最模糊的描述都讲不出来。
但是当模板型别推导规则应用于auto语境时,它们不像应用于模板时那样符合直觉。所以了解作为auto基础的模板型别推导的方方面面就变得相当重要了。
本条款将说明这些推导过程。这里通过一段伪代码来说明,函数模板大致形如:
template<typename T>
void f(ParamType param);
而一次调用形如:
f(expr); //以某表达式调用f
在编译期,编译器通过expr推导两个型别:一个是T的型别,另一个是ParamType的型别,这两个型别往往不一样。
因为,ParamType常会包含了一些饰词,如const或引用符号等限定词。例如,若模板声明如下:
template<typename T>
void f(const T& param); //ParamType是const T&
而调用语句如下:
int x = ;
f(x); //以一个int调用f
在此例中,T被推导为int,而ParamType则被推导为const int&。
上述情况,T的型别推导结果和传递给函数的实参型别是同一的,即T的型别就是expr的型别,x的型别是int,T的型别也被推导为int。但是这一点并不总是成立,T的型别推导结果,不仅仅依赖expr的型别,还依赖ParamType的形状。具体分为三种情况:
1、ParamType具有指针或引用型别,但不是万能引用;
2、ParamType是一个万能引用;
3、ParamType既非指针也非引用。
下面我们对这三种型别推到场景进行逐个考察。采用的还是前面所述的模板和调用形式:
template<typename T>
void f(ParamType param); f(expr); // 从expr来推导T和ParamType的型别
情况1:ParamType具有指针或引用型别,但不是万能引用
这种情况下,型别推导会这样运作:
1、若expr具有引用型别,先将引用部分忽略;
2、而后,对expr的型别和ParamType的型别执行模式匹配,来决定T的型别。
例如,我们的模式如下:
template<typename T>
void f(T& param); //Param是个引用
又声明了如下变量:
int x = ; //x的型别是int
const int cx = x; //cx的型别是const int
const int& rx = x; //rx是x的型别为const int的引用
在各次调用中,对param和T的型别推导结果如下:
f(x); //T的型别是int,param的型别是int&
f(cx); //T的型别是const int,param的型别是const int&
f(rx); //T的型别是const int,param的型别是const int&
在第二个和第三个调用语句中,由于cx和rx的值都被指明为const,所以T的型别被推导为const int,从而形参的型别就变成了const int&。
对于调用者,当向引用型别的形参传入const对象时,他们期望该对象保持不可修改的属性,也就是说,期望形参成为const的引用型别。这就保证了向持有T&型别的模板传入const对象是安全的:该对象的常量性(constness)会成为T的型别推导结果的组成部分。
第三个调用中,即使rx具有引用型别,T也未被推导为一个引用。原因在于,rx的引用性(reference-ness)会在型别推导过程中被忽略。
如果将形参型别从T&改为const T&,结果会有点变化。cx和rx的常量性仍然得到了满足,由于现在回假定param具有const引用型别,T的型别推导结果中包含的const也就没有必要了。
template<typename T>
void f(const T& param); //Param是个const引用 int x = ; //x的型别是int
const int cx = x; //cx的型别是const int
const int& rx = x; //rx是x的型别为const int的引用 f(x); //T的型别是int,param的型别是const int&
f(cx); //T的型别是int,param的型别是const int&
f(rx); //T的型别是int,param的型别是const int&
一如前例,rx的引用性在型别推导过程中是被忽略的。
param是指针而非引用时,其推导方式也是一样的:
template<typename T>
void f(T* param); //param是个指针 int x = ; //x的型别是int
const int* px = x; //px是指涉到x的指针,型别为const int f(&x); //T的型别是int,param的型别是int*
f(px); //T的型别是const int,param的型别是const int*
情况2:ParamType是一个万能引用
此类形参的声明方式类似右值引用(即函数模板中持有型别形参T时,万能引用的声明型别写作T&&),但是当传入实参是左值时,其表现会有所不同。
1、如果expr是个左值,T和ParamType都会被推导为左值引用。这一结果具有双重的奇特之处:首先,这是在模板型别推导中,T被推导为引用型的唯一情形。其次,尽管在声明时使用的是右值引用语法,它的型别推导结果却是左值引用。
2、如果expr是个右值,则应用“常规”规则。
例如:
template<typename T>
void f(T&& param); //param是个万能引用 int x = ; //x的型别是int
const int cx = x; //cx的型别是const int
const int& rx = x; //rx是x的型别为const int的引用 f(x); //x是个左值,所以T的型别是int&,param的型别是int& f(cx); //cx是个左值,T的型别是const int&,
//param的型别是const int&
f(rx); //rx是个左值,T的型别是const int&,
//param的型别是const int&
f(); //27是个右值,所以T的型别是int,
//这样param的型别就是int&&
情况3:ParamType既非指针也非引用
当ParamType既非指针也非引用时,就是所谓的按值传递:
template<typename T>
void f(T param); //param按值传递
这样,无论传入的是什么,param都会是它的一个副本,也即一个全新对象。
param是一个全新对象促成了如何从expr推导出T的型别的规则:
1、一如之前,若expr具有引用型别,则忽略其引用部分;
2、忽略expr的引用性后,若expr是个const对象,也忽略const属性。若其是个volatile对象,也忽略之。
int x = ; //x的型别是int
const int cx = x; //cx的型别是const int
const int& rx = x; //rx是x的型别为const int的引用 f(x); //T和param的型别都是int f(cx); //T和param的型别都是int f(rx); //T和param的型别都是int
如上所示,即使cx和rx代表const值,param仍然不具有const型别。这是合理的,因为param是个完全独立于cx和rx存在的对象--是cx和rx的副本。从而cx和rx不可修改这一事实并不能说明param是否可以修改。所以expr的常量性和挥发性可以在推导param的型别时加以忽略:仅仅由于expr不可修改,并不能断定其副本也不可修改。
需要重点说明的是,const和volatile仅会在按值形参处被忽略。若形参是const的引用或指针,expr的常量性会在型别推导过程中加以保留。
但是,当expr是个指涉到const对象的const指针,且expr按值传递给param:
template<typename T>
void f(T param); //param按值传递 const char* const prt = "Hello world"; //ptr是个指涉到const对象的
//const 指针 f(ptr); //传递型别为const char* const 的实参
如上所示,星号右侧的const把ptr声明为const:ptr不可以指涉到其他内存位置,也不可被置为null;位于星号左侧的const将ptr指涉到的对象(字符串)声明为const,即字符串不可修改。在ptr被传递给f时,这个指针本身将会按比特复制给param,就是说,ptr这个指针自己会被按值传递。依照按值传递形参的型别推导规则,ptr的常量性会被忽略,param的型别会被推导为const char*,即一个可修改的、指涉到一个const字符串的指针。
在型别推导中,ptr指涉的对象的常量性会被保留,其自身的常量性则会在以复制方式创建新指针param的过程中被忽略。
数组实参
以上已经基本讨论完模板型别推导的主流情况,但还有一个边缘情况要了解。这种情况就是:数组型别有别于指针型别,尽管有时它们看起来可以互换。形成这种假设的主要原因是,在很多语境下,数组会退化成指涉到的其首元素的指针。下面这段代码能够通过编译,就是因为这种退化机制在发挥作用:
const char name[] = "Hello World"; //name的型别是const char[12] const char* ptrToName = name; //数组退化成指针
这里型别为const char*的指针是ptrToName是通过name来初始化的,而后者的型别是const char[13]。这两个型别(const char* 和const char[13])并不统一,但是因为数组到指针的退化规则地存在,上述代码能够通过编译。
但当一个数组传递给持有按值形参的模板时,又会怎么样呢?
template<typename T>
void f(T param); //持有按值形参的模板 f(name); //T和param的型别会被推导成什么呢?
我们观察到,并没有任何的函数形参具有数组型别。但是,下面的语法是合法的:
void myFunc(int param[]);
但是虽然数组声明可以按照指针声明方式加以处理,那就意味着myFunc可以等价地声明如下:
void myFunc(int* param);
这种数组和指针形参的等价性,是作为c++基础的C根源遗迹,它使得“数组和指针型别是一回事”这一假象愈加扑朔迷离。
由于数组形参声明会按照它们好像是指针形参那样加以处理,按值传递给函数模板的数组型别将被推导成指针型别。
这样的话,在模板f的调用中,其型别形参T会被推导成const char*:
f(name); //name是个数组,但T的型别却被推导成const char*
难点来了!尽管函数无法声明真正的数组型别的形参,它们却能够将形参声明成数组的引用!所以,如果我们修改模板f,指定按引用方式传递其实参,
template<typename T>
void f(T& param); /按引用方式传递形参的模板 f(name); //向f传递一个数组
在这种情况下,T的型别会被推导成实际的数组型别!这个型别中会包含数组尺寸,在本例中,T的型别推导结果是const char[12],而f的形参(该数组的一个引用)型别被推导成const char(&) [12]。
函数实参
数组并非C++中唯一可以退化为指针之物。函数型别也同样可退化成函数指针,并且我们针对数组型别推导的一切讨论都适用于函数及其向函数指针的退化。所以结果如下:
void someFunc(int ,double); //someFunc是个函数,
//其型别是void(int ,double)
template<typename T>
void f1(T param); //f1中,param按值传递 template<typename T>
void f2(T& param); //f2中,param按引用传递 f1(someFunc); //param被推导为函数指针,
//具体型别是void(*)(int, double) f2(someFunc); //param被推导为函数引用,
//具体型别是void(&)(int, double)
在实践中,这些型别推导结果和前面讲过的没有什么不一样。
要点速记:
1、在模板推导的过程中,具有引用型别的实参会被当成非引用型来处理,就是说,其引用性会被忽略掉;
2、对万能引用形参进行推导时,左值实参会进行特殊处理;右值实参则按照情况1处理;
3、对按值传递的形参进行推导时,若实参型别中带有const或volatile饰词,则它们还是会被当作不带const或volatile饰词的型别处理;
4、在模板型别推导过程中,数组或函数型别的实参会退化成对应的指针,除非它们被用来初始化引用。
Effective Modern C++ 条款1:理解模板型别推导的更多相关文章
- Effective Modern C++ ——条款2 条款3 理解auto型别推导与理解decltype
条款2.理解auto型别推导 对于auto的型别推导而言,其中大部分情况和模板型别推导是一模一样的.只有一种特例情况. 我们先针对auto和模板型别推导一致的情况进行讨论: //某变量采用auto来声 ...
- Effective Modern C++ ——条款6 当auto型别不符合要求时,使用带显式型别的初始化物习惯用法
类的代理对象 其实这部分内容主要是说明了在STL或者某些其他代码的容器中,在一些代理类的作用下使得最后的返回值并不是想要的结果. 而他的返回值则是类中的一个容器,看下面的一段代码: std::vect ...
- Effective Modern C++ 条款2:理解auto型别推导
在条款1中,我们已经了解了有关模板型别的推导的一切必要知识,那么也就意味着基本上了解了auto型别推导的一切必要知识. 因为,除了一个奇妙的例外情况,auto型别推导就是模板型别推导.尽管和模板型别推 ...
- Effective Modern C++ 条款4:掌握查看型别推导结果的方法
采用何种工具来查看型别推导结果,取决于你在软件开发过程的哪个阶段需要该信息.主要研究三个可能的阶段:撰写代码阶段.编译阶段.运行时阶段. IDE编译器 IDE中的代码编译器通常会在你将鼠标指针选停止某 ...
- Effective Modern C++ 条款3:理解decltype
说起decltype,这是个古灵精怪的东西.对于给定的名字或表达式,decltype能告诉你该名字或表达式的型别.一般来说,它告诉你的结果和你预测的是一样的.不过,偶尔它也会给出某个结果,让你抓耳挠腮 ...
- Effective Modern C++ ——条款5 优先选择auto,而非显式型别声明
条款5 对于auto ,他的好处不仅仅是少打一些字这么简单. 首先在声明的时候, 使用auto会让我们养成初始化的习惯: auto x;//编译不通过必须初始化. 再次对于auto而言,它可以让我们定 ...
- Effective Modern C++ ——条款7 在创建对象时注意区分()和{}
杂项 在本条款的开头书中提到了两个细节性问题: 1.类中成员初始化的时候不能使用小括号. 如: class A { int a(0);//错误 }; 2.对于原子性类别的对象初始化的时候不能使用= 如 ...
- Effective Modern C++翻译(3)-条款2:明白auto类型推导
条款2 明白auto类型推导 如果你已经读完了条款1中有关模板类型推导的内容,那么你几乎已经知道了所有关于auto类型推导的事情,因为除了一个古怪的例外,auto的类型推导规则和模板的类型推导规则是一 ...
- 《Effective Modern C++》翻译--条款2: 理解auto自己主动类型推导
条款2: 理解auto自己主动类型推导 假设你已经读过条款1关于模板类型推导的内容,那么你差点儿已经知道了关于auto类型推导的所有. 至于为什么auto类型推导就是模板类型推导仅仅有一个地方感到好奇 ...
随机推荐
- 初识OpenCV-Python - 006: 图像的几何变换
本次小节学习了图像的变换,主要应用到如下方法: cv2.resize(), cv2.warpAffine(), cv2.getRotationMatrix2D(), cv2.getAffineTran ...
- 使用CEfSharp之旅(2) js前台事件执行后台方法
原文:使用CEfSharp之旅(2) js前台事件执行后台方法 版权声明:本文为博主原创文章,未经博主允许不得转载.可点击关注博主 ,不明白的进群191065815 我的群里问 https://blo ...
- Java多线程设计模式系列
通过几天的认真阅读,发现这是一本难得一见的好书,为了加深巩固学习成功,我打算将书中的例子全部自己实现一遍,特此记录下来也方便其他朋友学习. 第一章,java语言的线程 单线程程序:打印10000次go ...
- android 中的一些小问题
1 TextView 在TableRow 中占满一行 要为TextView设置 android:layout_weight="1" 这个属性 2
- web.xml中多个Servlet执行顺序的问题!
1.两个servlet或者两个servlet-mapping,其中的servlet-name名称不能存在相同. 2.所有的servlet-mapping标签下,url-pattern中包含的文本不能相 ...
- MySQL操作表和表记录
目录 操作表 增 列约束 列类型 删 改 修改表名 增加字段 修改字段 删除字段 查 复制表结构 操作表数据 增 删 改 查 操作表 增 创建表语法 创建一个表,多个字段: create table ...
- BZOJ2226:[SPOJ5971]LCMSum
Description Given n, calculate the sum LCM(1,n) + LCM(2,n) + .. + LCM(n,n), where LCM(i,n) denotes t ...
- Android基础控件TextView
1.常用属性 <TextView android:id="@+id/text11" //组件id android:layout_width="match_paren ...
- Xcode导航栏功能简介
1.Xcode 1.1.AboutXcode 1.2.Preferences General Accounts Behaviors1 Behavior2 Navigation Fonts& ...
- php面向对象成员方法(函数)练习
<?php header('content-type:text/html;charset=utf-8'); //成员方法的举例 /* ①添加sayHello 成员方法,输出 'hello' ②添 ...