前言
很高兴见到你。
类型推断,在我们使用模板的时候,是一个非常重要的话题。在c++11中,引入了auto关键字,也使得类型推断不局限于模板类型推断。举个简单的例子:
template<typename T>
void show(T t) {
std::cout << t << std::endl;
}
int i = 1;
int& num = i;
int array[8];
show(i); // 类型T被推断为int
show(num); // 类型T也是被推断为int
show(array); // 类型T被推断为int*
当我们传递参数给模板函数show
时,编译器会根据我们的参数类型去推断模板类型T
的类型。如果你对上面的代码的类型推断出现了疑惑,那么阅读这篇文章能够给你带来帮助。
对类型推断不熟悉,可能会带来一些很隐蔽的问题。例如上面的引用数据类型传递给show
函数时,发生了值拷贝,因为T
被推断成了int
类型,有些读者可能会有为什么不是int&
,为何会发生拷贝等问题。
模板匹配
值模板
参考以下代码:
// 模板类型T,没有前置const、volatile修饰,也没有指针、引用修饰
template<typename T>
void show(T t) {
std::cout << t << std::endl;
}
int x = 1;
const int y = 2;
int& z = x;
const int& w = y;
show(x); // T = int, t = int
show(y); // T = int, t = int
show(z); // T = int, t = int
show(w); // T = int, t = int
代码中的模板类型只有一个T
,不是T&
等,我这里称之为值模板。这种类型的模板,采用的为值拷贝,会移除参数的const
、volatile
、以及引用修饰。所以最终类型推断的T
均为int
类型。形参t
的类型即为T
,因此其类型和T
保持一致。
此模板类型的核心在于值拷贝,那么也不难理解,拷贝之后其实参修饰都变得没有意义了。
指针类型在与值模板的关联下会出现一些可能比较模糊的关系,但实际上他也是符合上面我们所说的逻辑的。参考以下代码:
template<typename T>
void show(T t) {
std::cout << t << std::endl;
}
int x = 1;
const int y = 2;
const int* p1 = &y;
const int* const p2 = &y;
int* const p3 = &x;
show(p1); // T = const int*,t = const int*
show(p2); // T = const int*,t = const int*
show(p3); // T = int*, t = int*
主要关注到show(p2);
。p2是一个指向const int的const 指针。当他匹配到T时,指针本身的const属性会被忽略,拷贝指针本身。但指针的类型不会被修改,还是指向const int数据。因此,类型推断结果为const int*。
引用模板
参考以下代码:
template<typename T>
void show(T& t) {
std::cout << t << std::endl;
}
int x = 1;
const int y = 2;
int& z = x;
const int& w = y;
show(x); // T = int, t = int&
show(y); // T = const int,t = const int&
show(z); // T = int, t = int&
show(w); // T = const int, t = const int&
在带有左值引用的模板函数中,形参t
都会被转化为左值引用类型。这里我强调是左值引用,因为右值引用有所不同,读者需要注意一下。t
的类型推到也非常符合直觉,并没有什么需要多余解释的。
模板T
则去除了引用修饰,这个好理解。因此T
后面跟了&
,那么他本身肯定是不带引用的。
同样的逻辑,我们也可以理解以下模板函数的类型推导:
template<typename T>
void show(const T& t) {
std::cout << t << std::endl;
}
show(x); // T = int,t = const int&
show(y); // T = int,t = const int&
show(z); // T = int, t = const int&
show(w); // T = int, t = const int&
形参模板增加了const
修饰,那么类型T
就会移除其const修饰。相对应的,t
类型会增加const
修饰,这不难理解是吧。
前面提到,这是一个左值引用。那么对于右值引用呢?参考以下代码:
template<typename T>
void show(T&& t) {
std::cout << t << std::endl;
}
int x = 1;
const int y = 2;
int& z = x;
const int& w = y;
show(x); // T = int&, t = int&
show(y); // T = const int&,t = const int&
show(z); // T = int&, t = int&
show(w); // T = const int&, t = const int&
show(1); // T = int, t = int&&
形如T&&
,没有const、volatile修饰,称为通用引用。通用引用可以适配左值引用与右值引用。通用引用的特性在于:他既能接收左值引用、又能接收右值引用。
因此我们对于上面的例子中的t
类型的推导其实并不奇怪。奇怪的在于T
的类型:对于右值的实参,T
被正常推导成int
,但是当实参是左值时,为什么会被推导成引用类型?
这其实涉及到一个概念:引用折叠。名字很高大上,但是引用折叠很简单:T& && = T&
。在c++中是不能存在引用的引用的,但是在类型推到的过程中会存储引用的引用。此时会将两个引用折叠,变成一个。所以就为什么T
类型带有左值引用了。
最后注意一下,只有T&&
类型的才被称为通用引用,而如果是const T&&
则不是通用引用,他是一个const的右值引用,无法接收左值引用。
指针模板
参考以下代码:
template<typename T>
void show(T* t) {
std::cout << &t << std::endl;
}
int x = 1;
const int y = 2;
show(&x); // T = int, t = int*
show(&y); // T = const int,t = const int*
指针类型和引用类型的类型推断逻辑几乎一模一样,也比较符合我们的直觉。
数组与函数
对于数组和函数类型参数,情况有些特殊。参考以下代码:
template<typename T>
void show(T t) {
//
}
template<typename T>
void showR(T& t) {
//
}
template<typename T>
void showP(T* t) {
//
}
int fun(int){return 0;}
int array[3] = {1,2,3};
show(fun); // T = int(*)(int),t = int(*)(int)
show(array); // T = int*, t = int*
showP(fun); // T = int(int),t = int(*)(int)
showP(array); // T = int, t = int*
showR(fun); // T = int(int),t = int(&)(int)
showR(array); // T = int[3], t = int &[3]
对于数组,首先我们需要明确一个概念:数组与指针是不一样的类型。。虽然在使用上很类似,但数组有长度、越界判断等,都是和指针不同的。
当我们把一个数组对象传递给T
类型时,数组类型会转化为指针类型,因此模板确定为int*
。相同的逻辑,对于T*
模板,自然模板也就会被初始化为int
。
函数是类似的,函数对象也会被转化成指针,因此模板T
会被初始化为指针类型,而指针类型的模板会被初始化为函数本身的类型。
最后再看到引用类型的模板。数组与函数对象,传递给引用类型的模板时,不会被退化成指针类型,而是会成为他本来的类型,且数组本身的长度也会被保留。我们可以利用这种方式来获取到数组的长度:
template<typename T, std::size_t N>
std::size_t arraySize(T (&u)[N])
{
return N;
}
int intArray[2]{0,1};
auto size = arraySize(intArray);
auto匹配
c++11增加了auto
关键字,将类型推断不仅局限于模板中。如以下代码:
auto num = 1; // int
auto& numRef = num; // int&
auto* numPtr = # // int*
auto temp = numRef; // int
在类型推断逻辑上,和模板基本是一模一样的,没有不同。唯一的不同在于:
auto list = {1,2,4}; // initialiez_list<int>
对于花括号数据,auto会直接推断为initialiez_list<Type>
,而模板是无法推断的。如以下代码是非法的:
template<typename T>
void show(T t){}
show({1,2,3}); // 无法推断T的类型
此外,auto还可以用于自定义函数返回值。如下:
// c++11
auto show() -> int {
// ...
}
// c++14 auto被推断为int
auto show() {
// ...
return 1;
}
在c++11中,我们需要后置指明auto的类型,而在c++14中,则可以通过最后一行代码来推断返回值类型。在c++11中,这似乎作用不是很大,但是我们可以结合模板和decltype来实现一些更加神奇的玩法:
template<typename T>
auto show(T t) -> decltype(Convert(t)) {
// ...
}
这里auto的类型为Convert(t)
,可以根据参数类型来决定函数的返回值类型。
总结
类型推断是c++模板编程中一个比较重要的内容,经过前面的学习,读者应该基本能掌握这部分的内容。在实际的开发中,有时候我们不确定其所推断的类型,可以通过开发IDE、typeid
等手段来判断类型,辅助我们确定具体的类型。
全文到此,原创不易,觉得有帮助可以点赞收藏评论转发。 有任何想法欢迎评论区交流指正。 如需转载请评论区或私信沟通。 另外欢迎光临笔者的个人博客:传送门