模版型别推导
函数模版大致形如
template<typename T>
void f(ParamType param);
模版函数的调用形如
f(expr);
在编译期,编译器会通过expr
推导两个类别。一个是T的类别,一个是ParamType
的类别。由于ParamType
常包含修饰词(如const T&
),ParamType
的类别可能会与T类别不同。
T的类别推导结果和传递给函数的实参类型不一定相同。T的类别推导结果,不仅仅与expr
类型有关,还与ParamType
的形式有关。分为以下三种情况
ParamType
具有指针或引用型别,但不是万能引用ParamType
是万能引用ParamType
即非指针又非引用
情况1:ParamType
是指针或引用,但不是万能引用
在此情况下,型别推导按以下步骤进行
- 若
expr
具有引用类别,先将引用部分忽略 - 尔后,对
expr
的型别和ParamType
的型别执行模式匹配,来决定T的型别
template<typename T>
void f(T& param);//param现为引用类型
int x = 0;
const int cx = x;
const int& rx = x;
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
还是param
都被推导为const
类型。向T&
模版传入const
类型的变量是安全的,该对象的常量性会成为推导结果的组成部分。右值引用的形参的推导方式与上述相同,但注意:传给右值引用形参的只能为右值引用实参。
当形参类别从T&
变为const T&
时:
template<typename T>
void f(const T& param);//param现为引用类型
int x = 0;
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&
T的推导类型可视为param
-const
,如const int
—> int
如果T
和param
是个指针:
template<typename T>
void f(T* param);//param现为引用类型
int x = 0;
const int* px = &x;
f(x);//T的类型是int,param类型为int*
f(px);//T的类型是const int,param类型为const int*
情况2:ParamType
是个万能引用
此类形参的声明方式类似右值引用(如函数模版中有类别T
时,形参声明为T&&
),但是当传入的实参是左值时,其表现会有所不同。
- 如果
expr
是个左值,T
和ParamType
都会被推导为左值引用。这是在模版型别推导中T被推导为引用型别的唯一类型,其次尽管声明时为右值引用,但型别推导的结果却是左值引用 - 如果
expr
是个右值,则应用常规方法
template<typename T>
void f(T&& param);
int x = 10;
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&&
由此可见,当遇到万能引用时,型别推导规则会区分实参是左值还是右值,而非万能引用不会进行这种区分。
情况3:ParamType即非指针又非引用
当ParamType
即非指针又非引用时,就到了按值传递的情况
template<typename T>
void f(T param);
这意味param
是实参的一份副本,即一个全新的对象。推导规则为:
- 若
expr
为引用型别,则忽略其引用部分 - 忽略
expr
的引用性之后,若expr
是一个const
对象或volatile
对象,也忽略之。所以
int x = 27;
const int cx = x;
const int& rx = x;
f(x);//T和param类型为int
f(cx);//同上
f(rx);//同上
如果expr是一个指向const
对象的const
指针呢?
template<typename T>
void f(T param);
const char* const ptr = "Hello World!";
f(ptr);//ptr的类型是const cahr*,还是char* const,还是char*?
虽然ptr
的指向不可改变,但ptr
在传递给f
时,ptr
这个指针自己会被按值传递,依照按值传递形参的型别推导规则,ptr
的常量性会被忽略,param
的类型会被推导为const char *
——指针指向可变的,指向一个不可变字符串的指针。
数组实参
数组型别有别于指针型别,尽管有时候它们看起来可以互换。形成这种假象的原因是数组可以退化成指向其首元素的指针。下面这段代码之所以能通过编译,就是因为这种退化机制在发挥作用
const char name[] = "Ada Wang";
const char* ptrToName = name;
在此情况下,型别为const char*
类型的指针是通过name
来初始化的,而后者的型别是const char[8]
,二者型别并不统一,代码能通过编译依赖于数组的退化规则。
由于数组退化导致的数组与指针的等价性,如果直接将数组传入按值传递的模版中,会被推导为指针类别
template<typename T>
void f(T param);
f(name);//T的类型被推导为const char*
但是,尽管函数无法声明真正的数组型别的形参,但却能够将形参声明为数组的引用。所以,如果修改模版,指定按引用的传递实参,然后向其传递一个数组
template<typename T>
void f(T& param);
f(name);//T推导为const char[13],f为const char(&)[13]
此时T的类型会被推导为实际的数组型别,甚至包含数组的长度。可以利用数组声明引用这一能力创造一个模版,用来推导出数组含有元素的个数
template<typename T, std::size_t N>
constexpr std::size_t arraysize(T (&)[N]) noexcept
{
return N;
}
然后可以初始化一个长度与未知数组相同的数组或其他STL类型
int keys = {1,2,3,4,5,6,7,8,9,10};
int cpy_keys[arraysize(keys)];
std::array<int,arraysize(keys)> exp;
函数实参
数组并非是C++中唯一可以退化为指针之物,函数型别亦可,并且对数组的推导规则对函数对象同样适用
void someFunc(int, double);
template<typename T>
void f1(T param);
template<typename T>
void f2(T& param);
f1(someFunc);//param被推导为函数指针,具体类型为void(*)(int, double)
f2(someFunc);//param被推导为函数引用,具体类型为void(&)(int, double)
总结
- 在模版型别的推导中,其引用性会被忽略
- 对万能引用形参进行推导时,左值实参会进行特殊处理
- 对按值传递的形参进行推导时,会消除实参中
const
和volatile
属性 - 在模版型别的推导中,数组或函数型别的实参会退化成对应的指针,除非模版形参为引用型
文章评论