条款1根据ParamType的特征,即一般形式的函数模板中param的型别饰词,将模板型别推导分为三种情况、而在采用auto进行变量声明中,型别饰词取代了ParamType,所以也存在三种情况:
- 情况1,型别饰词是指针或引用,但不是万能引用
- 型别饰词是万能引用
- 型别饰词既非指针也非引用
情况1和情况3:
auto x = 27; //x既非指针也非引用
const auto cx = x; //cx同样既非指针也非引用
const auto& rx = x; //rx是个引用,也不是万能引用
情况2的运作也和你的期望一致:
auto&& uref1 = x; //x的型别是int,且是左值。所以uref1的类型是int&
auto&& uref2 = cx; //cx的型别是const int, 且是左值,所以uref2的型别是const int&
auto&& uref3 = 25; //25的型别是Int,且是右值,所以uref3的型别是int&&
数组和函数名:
const char name[] = "R. N. Briggs"; //name的型别是const char[13]
auto arr1 = name; //arr1的型别是const char*
auto& arr2 = name; //arr2的型别是const char (&)[13]
void someFunc(int, double); //someFunc是个函数,型别是void(int, double)
auto func1 = someFunc; //func1的型别是void(*)(int, double)
auto& func2 = someFunc; //func1的型别是void(&)(int, double)
需要注意与模板推导不同点:
auto x1 = 27;
auto x2(27);
auto x3 = { 27 };
auto x4{ 27 };
这些声明都能通过编译,但结果却与一开始并不全部相同。前面两个语句确实仍然声明了一个Int型别为Int,值为27的变量,而后面两个语句,却声明了这么一个变量,其型别为std::initializer_list<int> ,且含有单个值为27的元素!
auto x1 = 27; //型别是Int,值是27
auto x2(27); //同上
auto x3 = { 27 }; //型别是std::initializer_list<int> ,值是{27}
auto x4{ 27 }; //同上
这是有关auto的一条特殊的型别推导规则所致,当用于auto声明变量的初始化表达式是使用大括号括起来时,推导所得的型别就属于std::initializer_list.这么一来,如果型别推导失败(例如,大括号里的值型别不一) ,则代码就不会通过编译
auto x5 = { 1, 2, 3.0}; //错误,推导不出initializer_list<T>中的T
当用于auto声明变量的初始化表达式是使用大括号括起来时,推导所得的型别就属于std::initializer_list。这么一来,如果型别推导失败(例如,大括号里的值型别不一),则代码就通不过编译:正如注释所指出的那样,这种情况下型别推导会失败,但是重点在于,要意识到这里发生了两种型别推导。第一种源于auto的使用:x5的型别需要推导。而x5的初始化表达式是用大括号括起来的,所以x5必须推导为一个std::initializer_list,但std::initializer_list是个模板,它药根据某个型别T产生实例型别std::initializer_list<T>,而这就意味着T的型别也必须推导出来。而这后一次推导就拉入了第二种型别推导,即模板型别推导的范畴了。在本例中,该推导会失败,因为大括号括起来的初始化表达式中的值型别不一。
对于大括号初始化表达式的处理方式,是auto型别推导和模板型别推导的唯一不同之处。当采用auto声明的变量使用大括号初始化表达式进行初始化时,推导所得的型别是std::initializer_list的一个实例型别。但是,如果向对应的模板传入一个同样的初始化表达式,型别推导就会失败,代码将不能通过编译:
auto x = {11, 23, 9}; //x的型别是std::initializer_list<int>
template<typename T> //带有形参的模板
void f(T param); //与x的声明等价的声明式
f({11, 23, 9}); //错误!无法推导T的型别
所以,auto和模板型别推导真正的唯一区别在于,auto会假定用大括号括起来的初始化表达式代表一个std::initializer_list,但模板型别推导却不会。
不过,你若指定该模板中param的为std::initializer_list<T>,则在T的型别未知时,模板型别推导机制会推导出T应有的型别:
template<typename T>
void f(std::initializer_list<T> initList);
f({11, 23, 9}); //T的型别推导为int,而从initList的型别std::initializer_list<int>
这条规则意味着当你采用auto来声明变量,而且初始化表达式是用大括号括起来的话,推导得到的型别总是std::initializer_list.极其重要的是,如果你想要拥抱统一初始化的哲学——就是说,会自然而然地把初始化值括在大括号里的话,那么务请牢记这条规则。C++11程序设计中的一个经典错误,就是明明把变量声明成了一个std::initializer_list,其实本意却并非如此,正式由于这个陷阱的存在,很多程序员都只在必要的时候才会使用大括号括起来的初始化表达式。
C++14允许使用auto来寿命函数返回值需要推导,而且C++14中的lambda式也会在形参声明中用到auto.然而,这些auto用法是在使用模板类型推导而非auto型别推导。所以,带有auto返回值的函数若要返回一个大括号括起来的初始化表达式,是通不过编译的:
auto createInitList()
{
return { 1, 2, 3}; //错误,无法为{1,2,3}完成型别推导
}
同样地,用auto来指定C++14中lambda式的形参型别时,也不能使用大括号括起来的初始化表达式:
std::vector<int> v;
auto resetV = [&v](const auto& newValue) { v = newValue; } //C++14
resetV({1,2,3}); //错误,无法为{1,2,3}完成型别推导
要记速点:
- 在一般情况下,auto型别推导和模板型别推导时一模一样的,但是auto型别推导会假定用大括号括起来的初始化表达式代表一个std::initializer_list,但模板型别推导却不会。
- 在函数返回值或lambda式的形参中使用auto,意思是使用模板型别推导而非auto型别推导。