c++类型推断解析

855 阅读3分钟

前言

很高兴见到你。

类型推断,在我们使用模板的时候,是一个非常重要的话题。在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&等,我这里称之为值模板。这种类型的模板,采用的为值拷贝,会移除参数的constvolatile、以及引用修饰。所以最终类型推断的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 = &num; // 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等手段来判断类型,辅助我们确定具体的类型。

全文到此,原创不易,觉得有帮助可以点赞收藏评论转发。 有任何想法欢迎评论区交流指正。 如需转载请评论区或私信沟通。 另外欢迎光临笔者的个人博客:传送门