理解Auto的类型推导

本文最后更新于:April 20, 2022 am

本系列博客主要讨论C++的类型推导系统

在上一节讨论模板的类型参数推导的基础上,本博客将会进一步讨论建立在此机制上auto关键字涉及的类型推导

当理解了上一节讨论的机制之后,实际上我们已经了解了auto关键字绝大多数的奥秘,回忆函数模板的一般使用过程:

1
2
3
4
5
template<typename T>
void f(ParamType param);

//调用
f(expr);

当编译器遇到对f的调用时,它使用expr的类型来推导ParamTypeT

使用auto时,auto关键字实际上扮演的上述函数中类型参数T的作用,连同其它的修饰符一起组成ParamType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
auto x = 27;
const auto xc= x;
const auto& rx=xc;

// 第一行的推导过程和此处的函数模板推导等价
template<typename T>
void func_for_x(T param);
func_for_x(x);

// 第二行的推导过程和此处的函数模板推导等价
template<typename T>
void func_for_xc(const T param);
func_for_xc(xc);

// 第三行的推导过程和此处的函数模板推导等价
template<typename T>
void func_for_rx(const T& param);
func_for_rx(rx);

不仅如此,上一节还讨论了3种情况和数组/函数的退化,auto关键字对此也十分受用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//case 1 + case 3:
auto x= 27; //case 3

const auto cx= x; //case 3

const auto& rx= x; //case 1

//case 2:
auto&& uref1 = x; //uref1 <->int&

auto&& uref2 = cx; //uref2 <->const int&

auto&& uref3 = rx; //uref3 <->const int&

auto&& uref4 = 27; //uref4 <->int&&

//数组退化:
const char note[] ="Programming in C++!";

auto arr1 = note; //arr1 <-> const char*

auto& arr2 = note; //arr2 <-> const char (&)[20]

//函数退化:
void someFunc(int ,double);

auto func1 = someFunc; //func1 <-> void(*)(int,double)

auto& func2 = someFunc; //func2 <-> void(&)(int,double)

auto和模板类型参数的推导过程,只有唯一的一点不同

区别

我们从这一段代码开始观察,如果需要声明一个初始值为27的int类型变量,C++98中,开发者有两种选择:

1
2
int x1 = 27;
int x2(27);

C++11以后,加入了统一形式的初始化物支持,以下两种方法也能实现

1
2
int x3 = {27};
int x4{27};

不论采用哪一种方法来声明变量,我们得到的结果都是一致的:一个int类型变量,初始值是27

正如Item5中所讲的,使用auto来替代固定的变量类型有好处,如果将int做字面意义上的替换得到:

1
2
3
4
auto x1 = 27;
auto x2(27);
auto x3 = {27};
auto x4{27};

替换后的代码可以编译,但是它们的行为和替换之前就不一样了:前两行仍然会声明int类型变量,随后赋予初始值27

问题是后两行,使用auto关键字后,转而声明了std::initializer_list<int>类型的变量,而非简单的int,编译后第三行的类型最终是std::initializer_list<int> ,第四行也是如此。

这里的区别便是auto和模板参数退推导过程不一致之处:当使用auto关键字声明的变量的初始值是大括号形式时,auto推导出的类型是std::initializer_list,如果无法推导出std::initializer_list的形式,编译器会报错:

1
2
auto x5 = {1, 2, 3.0};
//error! 不能推导std::initializer_list<T>中的T

此处其实有两个类型推导的过程,首先变量x5auto关键字声明,我们需要使用右侧的表达式来为其推导类型,右侧的表达式的类型也需要推导,而由于无法推导出std::initializer_list<T>T的类型,导致报错

上文提到大括号初始化物是auto和模板类型推导唯一的区别,那如果我们向函数模板中传入大括号初始化物,会发生什么呢?

1
2
3
4
5
6
7
8
9
auto x = {11, 23, 9};// ok, x is of type std::initializer_list<int>

template <typename T>void f(T param);

f({11,23,9});// error!

template <typename T>void f1(std::initializer_list<T> param);

f1({11,23,9}); //ok! T is int

区别就在此处,使用auto声明变量时,总是会假设大括号初始化物是std::initializer_list,而函数模板不会。不过至于为什么如此设计的原因,就需要我们自己去搜索引擎上寻找了。但是规则就是这样,我们需要牢记,当使用auto声明的变量的初始值是一个大括号初始化物时,变量的类型总是会被推导为std::initializer_list

对于C++11,故事到此就结束了,但是在C++14中,允许用auto来声明函数的返回值需要被推导,此外,lambda也可能使用auto来声明其参数的类型。好消息是,这两个新的特性使用的规则仍然是模板类型推导规则,而非auto类型推导——也就是说,当一个使用auto声明返回值的函数返回大括号初始化列表时,编译器会拒绝

1
2
3
4
auto createInitList(){
return {1,2,3};
}
//error! won't compile!

对于lambda的函数参数也是一样

1
2
3
4
5
6
std::vector<int>v;

auto resetV=[&v](const auto& newValue){
v = newValue;
}
resetV({1,2,3});//error!

总结

auto类型推导总是和模板类型推导采用相同的规则,而区别在于auto类型推导假设大括号初始化列表代表std::initializer_list,而模板类型推导没有这种假设

函数返回值是auto,或者lambda的参数类型是auto时,使用模板类型推导规则,而非auto类型推导规则


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!