成百上千的程序员都在向函数模板传递实参,并拿到了完全满意的结果,而这些程序员中却有很多对这些函数使用的型别是如何被推导出的过程连最模糊的描述都讲不出来。

  但是当模板型别推导规则应用于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:理解模板型别推导的更多相关文章

  1. Effective Modern C++ ——条款2 条款3 理解auto型别推导与理解decltype

    条款2.理解auto型别推导 对于auto的型别推导而言,其中大部分情况和模板型别推导是一模一样的.只有一种特例情况. 我们先针对auto和模板型别推导一致的情况进行讨论: //某变量采用auto来声 ...

  2. Effective Modern C++ ——条款6 当auto型别不符合要求时,使用带显式型别的初始化物习惯用法

    类的代理对象 其实这部分内容主要是说明了在STL或者某些其他代码的容器中,在一些代理类的作用下使得最后的返回值并不是想要的结果. 而他的返回值则是类中的一个容器,看下面的一段代码: std::vect ...

  3. Effective Modern C++ 条款2:理解auto型别推导

    在条款1中,我们已经了解了有关模板型别的推导的一切必要知识,那么也就意味着基本上了解了auto型别推导的一切必要知识. 因为,除了一个奇妙的例外情况,auto型别推导就是模板型别推导.尽管和模板型别推 ...

  4. Effective Modern C++ 条款4:掌握查看型别推导结果的方法

    采用何种工具来查看型别推导结果,取决于你在软件开发过程的哪个阶段需要该信息.主要研究三个可能的阶段:撰写代码阶段.编译阶段.运行时阶段. IDE编译器 IDE中的代码编译器通常会在你将鼠标指针选停止某 ...

  5. Effective Modern C++ 条款3:理解decltype

    说起decltype,这是个古灵精怪的东西.对于给定的名字或表达式,decltype能告诉你该名字或表达式的型别.一般来说,它告诉你的结果和你预测的是一样的.不过,偶尔它也会给出某个结果,让你抓耳挠腮 ...

  6. Effective Modern C++ ——条款5 优先选择auto,而非显式型别声明

    条款5 对于auto ,他的好处不仅仅是少打一些字这么简单. 首先在声明的时候, 使用auto会让我们养成初始化的习惯: auto x;//编译不通过必须初始化. 再次对于auto而言,它可以让我们定 ...

  7. Effective Modern C++ ——条款7 在创建对象时注意区分()和{}

    杂项 在本条款的开头书中提到了两个细节性问题: 1.类中成员初始化的时候不能使用小括号. 如: class A { int a(0);//错误 }; 2.对于原子性类别的对象初始化的时候不能使用= 如 ...

  8. Effective Modern C++翻译(3)-条款2:明白auto类型推导

    条款2 明白auto类型推导 如果你已经读完了条款1中有关模板类型推导的内容,那么你几乎已经知道了所有关于auto类型推导的事情,因为除了一个古怪的例外,auto的类型推导规则和模板的类型推导规则是一 ...

  9. 《Effective Modern C++》翻译--条款2: 理解auto自己主动类型推导

    条款2: 理解auto自己主动类型推导 假设你已经读过条款1关于模板类型推导的内容,那么你差点儿已经知道了关于auto类型推导的所有. 至于为什么auto类型推导就是模板类型推导仅仅有一个地方感到好奇 ...

随机推荐

  1. barrel shifter, logarthmic shifter and funnel shifter

    1,shifter小集合 (1) simple shift 左移或右移补0 (2) arthmetic shift 左移补0,右移补符号位 (3) barrel shifter 桶型,顾名思义,应该头 ...

  2. 【JZOJ2679】跨时代

    description 钟逆时针而绕,恶物狰狞的倾巢,我谦卑安静地于城堡下的晚祷,压抑远古流窜的蛮荒暗号,而管风琴键高傲的说,那只是在徒劳.我的乐器在环绕,时代无法淘汰我霸气的皇朝. 你无法预言,因为 ...

  3. ros python

    https://www.ncnynl.com/archives/201611/1059.html python的节点需要对节点设置权限为可执行,在.py文件所在的路径执行如下命令 $ touch ta ...

  4. COGS 2479. [HZOI 2016] 偏序 (CDQ套CDQ)

    传送门 解题思路 四维偏序问题,模仿三维偏序,第一维排序,第二维CDQ,最后剩下二元组,发现没办法处理,就继续嵌套CDQ分治.首先把二元组的左右两边分别打上不同的标记,因为统计答案时只统计左边对右边的 ...

  5. zabbix被监控端代理设置

    zabbix被监控端代理设置 安装zabbix-agent客户端 rpm -ivh https://repo.zabbix.com/zabbix/4.0/rhel/7/x86_64/zabbix-re ...

  6. 数据库DSN是什么

    数据库建立好之后,要设定系统的 DSN(数据来源名称),才能让网页可以知道数据库所在的位置以及数据库相关的属性.使用DSN的好处还有,如果移动数据库档案的位置,或是换成别种类型的数据库,只要重新设定 ...

  7. CDN与智能DNS原理和应用

    1.cdn概念,DNS概念 CDN:Centent Delivery Network(内容分发网络) 使用户可以就近取得所需内容,提高用户访问网站相应速度 CDN=更智能的镜像+缓存+流量导流: DN ...

  8. Windows操作系统下创建进程的过程

    进程(Process)是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位.程序只是一组指令的有序集合,它本身没有任何运行的含义,只是一个静态实体.而进程则 ...

  9. Error:Exception in thread "main" java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory

    ylbtech-Error:Exception in thread "main" java.lang.NoClassDefFoundError: org/slf4j/LoggerF ...

  10. Spring MVC(十一)--使用字符串实现重定向

    Spring MVC中有两种重定向方式: 通过返回字符串,字符串必须以redirect:开头: 通过返回ModelAndView: 重定向的时候如果需要给重定向目标方法传参数,要分字符串参数和pojo ...