现代C++之理解模板类型推断(template type deduction)
理解模板类型推断(template type deduction)
我们往往不能理解一个复杂的系统是如何运作的,但是却知道这个系统能够做什么。C++的模板类型推断便是如此,把参数传递到模板函数往往能让程序员得到满意的结果,但是却不能够比较清晰的描述其中的推断过程。模板类型推断是现代C++中被广泛使用的关键字auto的基础。当在auto上下文中使用模板类型推断的时候,它不会像应用在模板中那么直观,所以理解模板类型推断是如何在auto中运作的就很重要了。
下面将详细讨论。看下面的伪代码:
template<typename T>
void f(ParamType param);
通过下面的代码调用:
f(expr); //call f with some expression
在编译过程中编译器会使用expr推断两种类型:一个T的类型,一个是ParamType。而这两种类型往往是不一样的,因为ParamType通常会包含修饰符,比如const或者引用。如果一个模板被声明为下面这个样子:
template<typename T>
void f(const T& param);//ParamType is const T&
通过如下代码调用:
int x = 0;
f(x); //call f with an int
T会被推断成int,但是 ParamType会被推断成const int&。
我们很自然的会认为T的推断类型和传递到函数的参数类型是相同的,上面的例子就是这样的,参数x的类型为int,T也被推断成了int类型。但是往往情况不是这样子的。对T的类型推断不仅仅依赖参数expr的类型,也依赖ParamType的形式。
有三种情况:
- ParamType是指针或者引用类型,但不是universal reference(这个类型在以后的篇章中会讲到,现在只需要明白,这种类型不同于左值引用和右值引用即可。)
- ParamType是universal reference。
- ParamType即非指针也非引用。
下面将分别进行举例,每个例子都从下面的模板声明和函数调用伪代码演变而来:
template<typename T>
void f(ParamType param);
f(expr);
ParamType是指针或者引用类型
这种情况下的类型推断会是下面这个样子:
- 如果expr的类型是引用,忽略引用部分。
- 然后将expr的类型同ParamType进行模式匹配来最终决定T。
看下面的例子:
template <typename T>
void f(T ¶m);
声明如下变量:
int x = 27; //x 为int
const int cx = x;//cx为const int
const int& rx = x;//rx为指向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会变为类型的一部分。
第三个例子中,rx的类型是引用类型,T却被推断为非引用类型。因为类型推断过程中rx的引用类型会被忽略。
上面的例子只是说明了左值引用参数,对于右值引用参数同样试用。
如果我们将函数f的参数类型改成cont T&,实参cx和rx的const属性肯定不会变,但是现在我们将参数声明成为指向const的引用了,因此没有必要将const推断成为T的一部分:
template <typename T>
void f(const T ¶m);
声明的变量不变:
int x = 27; //不变
const int cx = x;//不变
const int& rx = x;//不变
对param和T的推断如下:
f(x); //T被推断为int,param的类型被推断为const int &
f(cx);//T被推断为int,param的类型被推断为const int &
f(rx);//T被推断为int(引用同样被忽略) ,param的类型被推断为const int &
如果param是指针或者指向const的指针,本质上同引用的推断过程是相同的。
指针和引用作为模板参数在推断过程中的结果是显而易见的,下面的例子就隐晦一些了。
ParamType是一个Universal Reference
这种类型的参数在声明时形式上同右值引用类似(如果一个函数模板的类型参数为T,将其声明为Universal Reference写成TT&&),但是传递进来的实参如果为左值,结果同右值引用就不太一样了(以后会讲到)。
Universal Reference的模板类型推断将会是下面这个样子:
- 如果expr是一个左值,T和ParamType都会被推断成左值引用。有点不可思议,首先,这是模板类型推断中唯一将T推断为引用的情况;其次,虽然ParamType的声明使用右值引用语法,但它最终却被推断成左值引用。
- 如果expr是一个右值,参考上一节(ParamType是指针或者引用类型)。
举个例子:
template <typename T>
void f(T &¶m);
int x = 27; //不变
const int cx = x;//不变
const int& rx = x;//不变
对param和T的推断如下:
f(x); //x为左值,因此T为int&,ParamType为 int&
f(cx);//cx为左值,因此T为const int&,ParamType也为const int&
f(rx);//rx为左值,因此T为const int&,ParamType也为const int&
f(27);//27为右值,T为int ,ParamType为int&&
这里的关键点是,模板参数为Universal Reference类型的时候,对于左值和右值的推断情况是不一样的。这种情况在模板参数为非Universal Reference类型的时候是不会发生的。
ParamType既不是指针也不是引用
这种情况也就是所谓的按值传递:
template <typename T>
void f(T param);//按值传递
传递到函数f中的实参值会是原来对象的一份拷贝。这决定了如何从expr中推断T:
- 同情况一类似,如果expr的类型是引用,忽略引用部分。
- 如果expr是const的,同样将其忽略。如果是volatile的,同样忽略。
看例子:
int x = 27; //不变
const int cx = x;//不变
const int& rx = x;//不变
对param和T的推断如下:
f(x); // T为int ParamType为 int
f(cx);//同上
f(rx);//同上
可以看到即使cx和rx为const,param也不是const的。因为param只是cx和rx的一份拷贝,所以不论param的类型如何都不会对原值造成影响。不能修改expr并不意味着不能修改expr的拷贝。
注意只有param是by-value的时候,const或者volatile才会被忽略。我们在前面的例子中说明了,如果参数类型为指向const的引用或者指针,类型推断过程中expr的const属性会被保留。但是看一下下面的情况,如果expr为指向const对象的const指针,而param的类型为by-value,结果会是什么样子的呢:
template <typename T>
void f(T param);//按值传递
const char * const ptr = "Fun with pointers";
f(ptr);
我们先回忆一下const指针,星号左边的const(离指针最近)表示指针是const的,不能修改指针的指向,星号右边的const表示指针指向的字符串是const的,不能修改字符串的内容。当ptr传递给f的时候,指针本身是按值传递的。因为在by-value参数的类型推断中const属性会被忽略,因此指针的const也就是星号右边的const会被忽略,最后推断出来的参数类型为const char * ptr,也就是可以修改指针指向,不能修改指针所指内容。
数组参数
上面的三种情况涵盖了模板类型推断的大部分情况,但是有另外一种情况不得不说,就是数组。虽然数组和指针有时候看上去是可以互换的,造成这种幻觉的一个主要原因是在许多情况下,数组可以退化为指向第一个数组元素的指针,正是这种退化下面的代码才能编译通过:
const char name[]="HarlanC";//name的类型为const char[8]
const char*ptrToName = name;//数组退化成指针
虽然指针和数组的类型不同,但由于数组退化为指针的规则,上边的代码能够编译通过。
如果将数组传递给带有by-value参数的模板,会发生什么呢?
template <typename T>
void f(T param);//按值传递
f(name);
将数组作为函数参数的语法是合法的。
void myFunc(int param[]);
但是这里的数组参数会被当做指针参数来处理,也就是说下面的声明和上面的声明是等价的:
void myFunc(int* param); // same function as above
因为数组参数会被当做指针参数来处理,所以将一个数组传递给按值传递的模板函数会被推断为一个指针类型。当调用模板函数f的时候,类型参数T会被推断成const char*:
f(name); // name is array, but T deduced as const char*
虽然函数不能声明一个真正的数组参数(即使这么声明也会被当做指针来处理),但是能够将参数声明为指向数组的引用。我们将模板函数做如下修改:
template <typename T>
void f(T& param);//按引用传递
传递一个数组实参:
f(name);
这时候会将T推断成一个真正的数组类型。这个类型同时包含了数组的大小,在上面的例子中,T会被推断成const char [8],而f的参数类型为const char (&)[8]。
使用这种声明有一个妙用。我们可以创建一个模板来推断出数组中包含的元素数量:
//在编译期返回数组大小 ,
//注意下面的函数参数是没有名字的
//因为我们只关心数组的元素数量
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
return N;
}
将函数返回值声明成constexpr类型的意味着这个值在编译期就能够得到。这样我们可以在编译期获取一个数组的大小,然后声明另外一个相同大小的数组:
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 };
int mappedVals[arraySize(keyVals)];
使用std::array更能够体现你是一个现代C++程序员:
std::array<int, arraySize(keyVals)> mappedVals;
函数参数
数组不是能够退化成指针的唯一类型。函数类型也能够退化为指针,我们所讨论的关于数组的类型推断过程同样适用于函数:
void someFunc(int, double); // someFunc是一个函数,类型为void(int, double)
template<typename T>
void f1(T param); //passed by value
template<typename T>
void f2(T& param); // passed by ref
f1(someFunc); // param 被推断为 ptr-to-func void (*)(int, double)
f2(someFunc); // param 被推断为ref-to-func void (&)(int, double)
要点总结
- 模板类型推断会把引用当做非引用来处理,也就是说会把参数的引用属性忽略掉。
- 当模板参数类型为universal reference 时,进行类型推断会对左值入参做特殊处理。
- 当模板类型参数为by-value时,const或者volatile会被当做非const或者非volatile处理。
- 当模板类型参数为by-value时,入参为函数或者数组时会退化为指针。
现代C++之理解模板类型推断(template type deduction)的更多相关文章
- C++ Templates (1.2 模板实参推断 Template Argument Deduction)
返回完整目录 目录 1.2 模板实参推断 Template Argument Deduction 1.2 模板实参推断 Template Argument Deduction 当调用函数模板(如max ...
- [Effective Modern C++] Item 1. Understand template type deduction - 了解模板类型推断
条款一 了解模板类型推断 基本情况 首先定义函数模板和函数调用的形式如下,在编译期间,编译器推断T和ParamType的类型,两者基本不相同,因为ParamType常常包含const.引用等修饰符 t ...
- 现代C++之理解auto类型推断
理解auto类型推断 上一篇帖子中讲述了模板类型推断,我们知道auto的实现原理是基于模板类型推断的,回顾一下模板类型推断: template <typename T> void f(Pa ...
- c/c++ 模板 类型推断
模板类型的推断 下面的函数f是个模板函数,typename T.下表是,根据调用测的实参,推断出来的T的类型. 请注意下表的红字部分, f(T&& t)看起来是右值引用,但其实它会根据 ...
- c++11-17 模板核心知识(三)—— 非类型模板参数 Nontype Template Parameters
类模板的非类型模板参数 函数模板的非类型模板参数 限制 使用auto推断非类型模板参数 模板参数不一定非得是类型,它们还可以是普通的数值.我们仍然使用前面文章的Stack的例子. 类模板的非类型模板参 ...
- c++11-17 模板核心知识(五)—— 理解模板参数推导规则
Case 1 : ParamType是一个指针或者引用,但不是universal reference T& const T& T* Case 2 : ParamType是Univers ...
- C++11新特性:自动类型推断和类型获取
声明:本文是在Alex Allain的文章http://www.cprogramming.com/c++11/c++11-auto-decltype-return-value-after-functi ...
- [转]Traits 编程技法+模板偏特化+template参数推导+内嵌型别编程技巧
STL中,traits编程技法得到了很大的应用,了解这个,才能一窥STL奥妙所在. 先将自己所理解的记录如下: Traits技术可以用来获得一个 类型 的相关信息的. 首先假如有以下一个泛型的迭代器类 ...
- 【C++ Primer 第16章】2. 模板实参推断
模板实参推断:对于函数模板,编译器利用调用中的函数实参来确定模板参数,从函数实参来确定模板参数的过程被称为模板实参推断. 类型转换与模板类型参数 与往常一样,顶层const无论在形参中还是在是实参中, ...
随机推荐
- iOS 运行时使用(交换两个方法)
举例 在创建了如下代码 NSString *str=nil; NSURL *url =[NSURL URLWithString:str]; NSLog(@"%@",url); 但是 ...
- D3.js force力导向图用指定的字段确定link的source和target,默认是索引
json.links.forEach(function (e) { var sourceNode = json.nodes.filter(function (n) { return n.name == ...
- linux的systemctl服务及其使用
一.systemd 系统初始化程序,系统开始的第一个进程,PID为1 二.systemctl命令 systemctl list-units ##列出当前系统服务的状态 systemctl lis ...
- spring cloud 声明式rest客户端feign调用远程http服务
在Spring Cloud Netflix栈中,各个微服务都是以HTTP接口的形式暴露自身服务的,因此在调用远程服务时就必须使用HTTP客户端.Feign就是Spring Cloud提供的一种声明式R ...
- Python - 去除list中的空字符
list1 = ['122', '2333', '3444', '', '', None] a = list(filter(None, list1)) # 只能过滤空字符和None print(a) ...
- settings.py常见配置项
settings.py常见配置项 1. 配置Django_Admin依照中文界面显示 LANGUAGE_CODE = 'zh-hans' 2. 数据库配置(默认使用sqlite3) 1 .默认使用的s ...
- 一脸懵逼加从入门到绝望学习hadoop之 org.apache.hadoop.ipc.RemoteException(org.apache.hadoop.security.AccessControlException): Permission denied: user=Administrator, access=WRITE, inode="/":root:supergroup:drwxr-xr报错
1:初学hadoop遇到各种错误,这里贴一下,方便以后脑补吧,报错如下: 主要是在window环境下面搞hadoop,而hadoop部署在linux操作系统上面:出现这个错误是权限的问题,操作hado ...
- Zabbix 3.2.6通过SNMP和iDRAC监控DELL服务器
https://www.cnblogs.com/saneri/p/7772641.html
- Tarjan算法【强连通分量】
转自:byvoid:有向图强连通分量的Tarjan算法 Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树.搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断 ...
- 【bzoj2131】免费的馅饼 dp+树状数组
题解: 昨天好像做了个几乎一模一样的题目 按照ti排序 |p[i]-p[j]|<=2*(t[i]-t[j]) 然后去绝对值变为三维偏序 发现后两个式子可以推出ti<tj 所以就变成二维偏序 ...