条款1:理解模板类型推导

发布于:2025-08-20 ⋅ 阅读:(17) ⋅ 点赞:(0)

1.1 函数模板

函数模板看起来会是这样(伪码):

template<typename T>
void f(ParamType param);
f(expr); // 用一些表达式来调用f

在编译的时候,编译器通过 expr 推导出两个类型:T 和ParamType。通常来说T和ParamType是不同的,因为 ParamType 通常包含一些类型的装饰:

template<typename T>
void f(const T& param); // ParamType 是 const T&

int x = 0;
f(x); // 使用int调用f

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

1.2 T 类型推导的三种情况

对于 T 的类型推导不仅取决于 expr 的类型,还取决于 ParamType 的形式。有三种情况:

1.2.1 ParamType 是引用或指针,但不是通用引用

  1. 如果 expr 的类型是个引用,忽略引用的部分
  2. 然后利用 expr 的类型和 ParamType 对比去判断 T 的类型
template<typename T>
void f(T& param); 	// param是一个引用类型
int x = 27; 		// x是一个int
const int cx = x; 	// cx是一个const int
const int& rx = x; 	// rx是const int的引用
f(x); 	// T是int,param的类型是int&
f(cx); 	// T是const int,param的类型是const int&
f(rx); 	// T是const int,param的类型是const int&

如果 f 的参数类型从 T& 变成 const T& ,情况就会发生变化:

template<typename T>
void f(const T& param); // param现在是const的引用

int x = 27; 		// 和之前一样
const int cx = x; 	// 和之前一样
const int& rx = x; 	// 和之前一样
f(x); 	// T是int,param的类型是const int&
f(cx); 	// T是int,param的类型是const int&
f(rx); 	// T是int,param的类型是const int&

如果 param 是一个指针(或者指向 const 的指针)而不是引用,情况也是类似:

template<typename T>
void f(T* param); 	// param是一个指针
int x = 27; 		// 和之前一样
const int *px = &x; 	// px是一个指向const int x的指针
f(&x); 	// T是int,param的类型是int*
f(px); 	// T是const int,param的类型时const int*

1.2.2 ParamType 是通用引用(Universal Reference)

通用引用参数的类型声明是 T&& ,对于通用的引用参数,情况就变得不是那么明显了。

  1. 如果 expr 是一个左值, T 和 ParamType 都会被推导成左值引用。
  2. 如果 expr 是一个右值,那么就执行“普通”的法则(第一种情况)。
template<typename T>
void f(T&& param); 	// param现在是一个通用的引用

int x = 27; 		// 和之前一样
const int cx = x; 	// 和之前一样
const int& rx = x; 	// 和之前一样
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); 	// 27是右值,所以T是int,param的类型是int&&

1.2.3 ParamType 既不是指针也不是引用

按pass-by-value处理:

  1. 如果 expr 的类型是个引用,将会忽略引用的部分。
  2. 如果expr 是 const 的,也要忽略掉 const 。如果是 volatile的,也要忽略掉。
template<typename T>
void f(T param); // param现在是pass-by-value
int x = 27; 		// 和之前一样
const int cx = x; 	// 和之前一样
const int& rx = x; 	// 和之前一样
f(x); 	// T和param的类型都是int
f(cx); 	// T和param的类型也都是int
f(rx); 	// T和param的类型还都是int
//cx 和 rx 不能被修改和 param 能不能被修改是没有关系的

指针自己( ptr )本身是被按值传递,则忽略指针本身的const:

template<typename T>
void f(T param); 				// param仍然是按值传递的(pass by value)
const char* const ptr = "Fun with pointers";// ptr是一个const指针,指向一个const对象
f(ptr); 					// 给参数传递的是一个const char * const类型
//param 推导出来的类型就是 const char* 

1.3 数组参数

虽然通常情况下,一个数组会被退化成一个指向其第一个元素的指针,数组类型和指针类型是不一样的:

const char name[] = "J. P. Briggs"; 	// name的类型是const char[13]
const char * ptrToName = name; 	// 数组被退化成指针
//(const char* 和 const char[13] 是不一样的类型,但基于数组到指针的退化规则,代码会被正常编译

模板拥有一个按值传递的参数,不能将数组作为函数参数。虽然,下面这样是合法的。

//数组声明被当作指针声明来处理
template<typename T>
void f(T param); 	// 模板拥有一个按值传递的参数
void myFunc(int param[]);   
void myFunc(int* param);    	// 和上面的函数是一样的
f(name); 			// name是个数组,但是T被推导成const char*

可以声明参数是数组的引用:

template<typename T>
void f(T& param); 	// 引用参数的模板
f(name); 		// 传递数组给f
// T 被推导成了 const char [13] ,函数 f 的参数(数组的引用)被推导成了 const char(&)[13]

通过声明数组的引用,可以创造出推导数组长度的模板:

// 在编译的时候返回数组的长度(数组参数没有名字,因为只关心数组包含的元素的个数)
template<typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept
{
    return N; 
}
int keyVals[] = { 1, 3, 7, 9, 11, 22, 35 }; // keyVals有7个元素
int mappedVals[arraySize(keyVals)]; 		// mappedVals长度也是7

数组的替代方案:

std::array<int, arraySize(keyVals)> mappedVals; // mappedVals长度是7

1.4 函数参数

函数类型可以被退化成函数指针:

void someFunc(intdouble); 	// someFunc是一个函数,类型是void(int, double)
template<typename T>
void f1(T param); 	// 在f1中 参数直接按值传递
template<typename T>
void f2(T& param); 	// 在f2中 参数是按照引用传递
f1(someFunc); 		// param被推导成函数指针,类型是void(*)(int, double)
f2(someFunc); 		// param被推导成函数引用,类型是void(&)(int, double)

1.5 要点速记

  1. 在模板类型推导期间,引用实参被视为非引用,即忽略其引用性质。
  2. 在推导通用引用形参的类型时,左值实参会得到特殊处理。
  3. 在推导按值传递的形参类型时,const 和/或 volatile 参数被视为非 const 和非volatile的。
  4. 在模板类型推导期间,数组或函数实参会退化为相应的指针,除非它们用于初始化引用。

网站公告

今日签到

点亮在社区的每一天
去签到