理解auto类型推断
如果你已经阅读了条款1,那么你基本上已经知道auto的规则了,因为除了一个奇怪的例外,auto的类型推断与模板类型推断相同。
auto类型推断可以被我们转换成模板类型推断。
在条款1中
template <typename T>
void f(ParamType param);
然后我们调用函数
f(expr);
在这个调用中,编译器通过expr 来推断T 的类型和ParamType 的类型。
而当我们用auto来声明定义变量时, auto 在上面的模板中扮演着T 的角色,而变量的类型则是ParamType ,很容易用以下例子来说明
auto x = 27;
在这里 x 的类型是auto,而在另一个例子中
const auto cx = x;
x 的类型是const auto,还有
const auto &rx = x;
x的类型是const auto & ,在这些例子中,编译器就像对待模板推断一样来对待auto,而相应的模板函数如下
template <typename T>
void func_for_x(T param);
func_for_x(27); // auto x = 27
template <typename T>
void func_for_cx(const T param);
func_for_cx(x); // const auto cx =x
template <typename T>
void func_for_rx(const T& param);
func_for_rx(x); // const auto& rx = x
所以我们才说,auto类型推断和模板类型推断基本相同,只有一个例外,这个例外我们很快就会讨论到。
在条款1的模板类型推断中,根据ParamType (也就是函数中实际参数的类型)的不同分成了三种情况,而在auto类型推断中,类型标识符的类型也就是ParamType,因此也分成三种情况。
- 类型标识符是指针或者引用,但是不是通用引用(universal reference)
- 类型标识符是通用引用(universal reference)
- 类型标识符既不是指针也不是引用
我们已经在第一个例子中看见情况1和情况3
auto x = 27;
// 情况3
const auto cx = x;
// 情况3
const auto &rx = x;
// 情况1
而第二种情况如下
auto&& uref1 = x;
// x是int类型而且是个左值,所以uref1的类型是int&
auto&& uref2 = cx;
// cx是const int类型而且是个左值,所以uref2的类型是const int&
auto&& uref3 = 27;
// 27是个右值,uref3的类型是int&&
条款1中关于数组和函数的讨论,在auto中也适用
const char name[] = "R. N. Briggs";
auto arr1 = name;
// arr1的类型是const char*
auto &arr2 = name;
// arr2的类型是const char (&)[13]
void someFunc(int, double);
auto fun1 = someFunc;
// func1的类型是void (*)(int, double)
auto &func2 = someFunc;
// func2的类型是void (&)(int, double)
所以说模板类型推断和auto类型推断真的很类似。
auto类型推断有一个地方和模板类型推断不一样。
当我们想声明定义一个int类型时,在C++98有两种选择
int x1 = 27;
int x2(27);
而在C++11之后,又多了两种声明定义方式
int x3 = { 27 };
int x4{ 27 };
这四种声明定义方式的结果都是相同的,一个int类型的值为27。
在条款5中会解释用auto声明定义比用具体类型好,所以在这里我们用auto代替int,如下
auto x1 = 27;
auto x2(27);
auto x3 = { 27 };
auto x4{ 27 };
而这样编译运行后,前两个表达式中的变量是类型为int,值为27;而后两个表达式的变量类型为std::initializer_list<int>
,该序列只有一个元素值为27。
这是因为auto类型推断中的一个特殊规则,当用auto声明定义的变量用一个封闭的大括号初始化时,auto会把这个变量推断为std::initializer_list类型。如果这样的变量无法推断类型(例如大括号内有不同类型的值),那么代码会报错:
auto x5 = { 1, 2, 3.0 };
// 报错,
//无法推断std::initializer_list<T>
中的T 的类型
上面的代码有两次类型推断,一次是通过括号把x5推断为std::initilizer_list<T>
,然后再用括号中的内容推断T 的类型
在auto中对于大括号初始化只有一种规则,就是把变量的类型推断为initializer_list,而相同的大括号放到模板类型推断中,类型推断会失败,而且代码也会报错:
auto x = { 11, 23, 9};
// x的类型为std::initializer_list<int>
template <typename T>
void f(T param);
f({ 11, 23, 9 });
// 报错,无法推断类型T
但是,如果你在模板中把参数的类型定为std::initializer_list<T>
,,然后让编译器来推断类型T,这时候代码是正确的:
template <typename T>
void f(std::initializer_list<T> initList);
f({ 11, 23, 9 });
// 此时T 的类型会被推断为int,而initList的类型是std::initialzer_list<int>
所以,auto类型推断和模板类型推断的不同之处在于,auto会假定大括号初始化时表现为initializer_list,而模板类型推断不会这样做。
在C++14中,可以把函数的返回值类型或者lambdad中的参数用auto声明,但是这两种情况下,类型推断原则应用的是模板类型推断的规则,所以在这两种情况下使用大括号是错误的:
auto createInitList()
{
return { 1, 2, 3 }; // 报错
}
同理
std::vector<int> v;
...
auto resetV = [&v](const auto& newValue) { v = newValue; };
...
resetV({ 1, 2, 3}); // 报错
总结
要记住的两点
- auto类型推断与模板类型推断基本一致,不过auto会假定大括号是一个std::initializer_list,但是模板类型推断则不会。
- 当一个函数返回类型是auto或者在lambda的参数中使用auto时,它们的类型推断规则是用模板类型推断的规则,而不是auto类型推断。