模板类型推导

模板类型推导


以下面伪代码为例:

template<class T>
void TemplateFun(ParamType param);

调用:

TemplateFun(expr);

在编译的时候,编译器通过expr来进行推导出两个类型:一个是T的,另一个是ParamType。通常来说这些类型是不同的,因为 ParamType 通常包含一些类型的装饰,比如const或引用特性,T的类型不仅和expr的类型独立,而且还和ParamType的形式独立

template<class T>
void TemplateFun(const T& param)
{
}

第一种调用方式:

{
	int nTmp = 0;
	TemplateFun(nTmp);
}

T 被推导成int,ParamType被推导成const int&

ParamType是个非通用的引用或者是一个指针


当 ParamType 是一个引用类型或者是一个指针,但并非是通用的引用。在这种情况下,类型推导的过程如下:

  1. 如果 expr 的类型是个引用,忽略引用的部分。
  2. 然后利用 expr 的类型和 ParamType 对比去判断 T 的类型

有如下模板函数:

template<class T>
void TemplateFun(T& param)
{
}

调用:

{
	int nTmp = 0;
	const int nConstTmp = 10;
	const int& nConstRefTmp = nTmp;

	TemplateFun(nTmp);
	TemplateFun(nConstTmp);
	TemplateFun(nConstRefTmp);
}

这种调用方式TemplateFun(nTmp);T是int,param的类型时int&

这种调用方式TemplateFun(nConstTmp);T是const int,param的类型时const int&

这种调用方式TemplateFun(nConstRefTmp);T是const int,param的类型时const int&,按照推导过程尽管 nConstRefTmp 的类型是一个引用, T 仍然被推导成了一个非引用的。这是因为 nConstRefTmp 的引用特性会被类型推导所忽略。

当传递一个 const 对象给一个引用参数,我们期望对象会保留常量特性,也就是说,参数变成了const的引用。这也就是为什么给一个以 T& 为参数的模板传递一个 const 对象是安全的:对象的 const 特性是 T 类型推导的一部分。

上述示例展示了左值引用参数的处理方式,但是类型推导在右值引用上也是如此。当然,右值参数只可能传递给右值引用参数,但是这个限制和类型推导没有关系

我们对上面的模板函数修改一下在看看推导出的类型:

template<class T>
void TemplateFun(const T& param)
{
}

调用依然一样

这种调用方式TemplateFun(nTmp);T是int,param的类型是const int&

这种调用方式TemplateFun(nConstTmp);T是int,param的类型是const int&

这种调用方式TemplateFun(nConstRefTmp);T是int,param的类型是const int&

对于指针也一样

template<class T>
void TemplateFun(T* param)
{
}

{
	int nTmp = 0;
	const int* nConstTmp = &nTmp;

	TemplateFun(&nTmp);
	TemplateFun(nConstTmp);
}

这种调用方式TemplateFun(&nTmp);T是int,param的类型是int*

这种调用方式TemplateFun(nConstTmp);T是const int,param的类型是const int*

ParamType是个通用的引用


对于通用的引用参数,这些参数被声明成右值引用(也就是函数模板使用一个类型参数 T ,一个通用的引用参数的声明类型是 T&& ),但是当传递进去右 值参数情况变得不一样

  • 如果expr是一个左值,T和ParamType都会被推导成左值引用。这有些不同寻常。第一,这是模板类型 T 被推导成一个引用的唯一情况。第二,尽管 ParamType利用右值引用的语法来进行推导,但是他最终推导出来的类型是左值引用

  • 如果 expr是一个右值,那么就执行“普通”的法则(第一种情况ParamType是个非通用的引用或者是一个指针)

例如:

template<class T>
void TemplateFun(T&& param)
{
}

{
	int nTmp = 0;
	const int nConstTmp = 10;
	const int& nConstRefTmp = nTmp;

	TemplateFun(nTmp);
	TemplateFun(nConstTmp);
	TemplateFun(nConstRefTmp);
	TemplateFun(100);
}

这种调用方式TemplateFun(nTmp);nTmp是个左值所以T是int&,param的类型是int&

这种调用方式TemplateFun(nConstTmp);nConstTmp是个左值所以T是const int&,param的类型是const int&

这种调用方式TemplateFun(nConstRefTmp);nConstRefTmp是个左值所以T是const int&,param的类型是const int&

而这种调用方式TemplateFun(100);100是右值所以T是int,param的类型是int&&

以后我们讨论这个例子推导的原因。关键的地方在于通用引用的类型推导法则和左值引用或 者右值引用的法则大不相同。特殊的情况下,当使用了通用的引用,左值参数和右值参数的 类型推导大不相同。这在非通用的类型推到上面绝对不会发生

ParamType 既不是指针也不是引用


当 ParamType 既不是指针也不是引用,我们把它处理成by-value,这就意味着param就是完全传给他的参数的一份拷贝——一个完全新的对象。基于这个事实 可以从 expr 给出推导的法则:

  1. 和之前一样,如果 expr 的类型是个引用,将会忽略引用的部分。
  2. 如果在忽略 expr 的引用特性, expr 是个 const 的,也要忽略掉 const 。如果 是 volatile ,照样也要忽略掉

例如:

template<class T>
void TemplateFun(T param);

{
	int nTmp = 0;
	const int nConstTmp = 10;
	const int& nConstRefTmp = nTmp;

	TemplateFun(nTmp);
	TemplateFun(nConstTmp);
	TemplateFun(nConstRefTmp);
}

这三种调用方式TemplateFun(nTmp);TemplateFun(nConstTmp);TemplateFun(nConstRefTmp);T和param的类型都是int

正如我们刚刚提到到by-value按值传递参数这种形式param 是一个独立的对象,expr不能被修改并不意味着它的一份拷贝不能被修改,所以nConstTmp和nConstRefTmp虽然是const类型,但是param却不是const的

下面这种调用:

const char* const ptr = "pointer";

TemplateFun(ptr);

我们知道位于星号右边的const是表明指针是常量const的:ptr不能被修改指向另外一个不同的地址,并且也不能置成null;星号左边的const表明ptr 指向的——字符串——是const的,也就是说字符串不能被修改。因为是by-value按值传递的,所以ptr的const特性会被忽略,这样param 的推导出来的类型就是const char* ,也就是一个可以被修改的指针,指向一个const的字符串。 ptr指向的东西的 const 特性被 加以保留,但是ptr自己本身的 const 特性会被忽略,因为它要被重新复制一份而创建了一个新的指针param

数组参数


数组类型和指针类型是不一样的,尽管它们通常看起来是可以替换的。一个最基本的幻觉就是在很多的情况下,一个数组会被退化成一个指向其第一个元素的指针。例如:

const char name[] = "zhangsan"; 
const char * ptrToName = name;

在这里, const char* 指针ptrToName使用name初始化,实际的name的类型是const char[8] 。这些类型( const char* 和 const char[8] )是不一样的,但是因为数组到指针的退化规则,代码会被正常编译

当以by-value按值传递的方式传递数组参数会被退化成指针类型,例如:

TemplateFun(name);

那么T被推导成const char*

我们修改一下模板函数的定义:

template<class T>
void TemplateFun(T& param);

那么此时的调用:

TemplateFun(name);

此时T最后推导出来的实际的类型就是数组!类型推导包括了数组的长度,所以在这个例子里面,T被推导成了const char [8] ,param的类型被推导成了const char(&)[8]

一个推导出一个数组包含元素长度的模板:

// 在编译的时候返回数组的长度(数组参数没有名字,因为只关心数组包含的元素的个数)
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
	return N;
}

这里关于constexpr的使用可以参考这篇文章

关于noexcept的使用可以参考这篇文章

函数参数


函数类型可以被退化成函数指针,它的退化和数组参数类似:

void ProcessFun(int nParam);

template<class T>
void TemplateFun(T param);

当以by-value按值传递的方式,此时调用TemplateFun(ProcessFun)param被推导成函数指针void(*)(int)

当模板函数的定义:

template<class T>
void TemplateFun(T& param);

此时调用TemplateFun(ProcessFun)param被推导成函数指针void(&)(int)

最后


  1. 在模板类型推导的时候,有引用特性的参数的引用特性会被忽略

  2. 在推导通用引用参数的时候,左值会被特殊处理

  3. 在推导按值传递的参数时候, const和volatile参数会被视为非const和非volatile

  4. 在模板类型推导的时候,参数如果是数组或者函数名称,他们会被退化成指针,除非是用在初始化引用类型