条款1中,模板类型推导的函数模板形如
template<typename T>
void f(ParamType param);
f(expr); //以某表达式调用f
在f
的调用语句中,编译器会利用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&
若要推导x,cx和rx的类型,编译器的行为就彷佛对应于每个声明生成了一个模板和一次使用对应的初始化表达式针对该模板的调用似的。
template<typename T> //为推导x的类型而生成的概念性模板
void func_for_x(T param);
func_for_x(27); //概念性调用语句:推导得出的param的类型就是x的类型
template<typename T> //为推导cx的类型而生成的概念性模板
void func_for_cx(const T param);
func_for_cx(x); //概念性调用语句:推导得出的param的类型就是cx的类型
template<typename T> //为推导rx的类型而生成的概念性模板
void func_for_rx(const T& param);
func_for_rx(x); //概念性调用语句:推导得出的param的类型就是rx的类型
条款1根据ParamType的特征,即一般形式的函数模板中param的类型饰词,将模板类型推导分成三种情况。在采用auto
进行变量声明中,类型饰词取代了ParamType,所以也存在三种情况。
- 情况1:类型饰词是指针或引用,但不是万能引用
- 情况2:类型饰词是万能引用
- 情况3:类型饰词既非指针也非引用
//情况1和情况3
auto x = 27; //情况3(x既非指针也非引用)
const auto cx = x; //情况3(cx同样既非指针也非引用)
const auto& rx = x; //情况1(rx是个引用,但不是万能引用)
//情况2
auto&& uref1 = x; //x的类型是int,且是左值,所以uref1的类型是int&
auto&& uref2 = cx; //cx的类型是const int,且是左值,所以uref2的类型是const int&
auto&& uref3 = 27; //27的类型是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; //func2的类型是void (&)(int, double)
若要声明一个int
,并将其初始化为值27,c++98中有两种可选语法:
int x1 = 27;
int x2(27);
c++11增加了下面的语法
int x3 = {27};
int x4{27};
条款5所述,采用auto
声明变量,相比采用固定类型声明变量更有优势。
auto x1 = 27;
auto x2(27);
auto x3 = { 27 };
auto x4{27};
前面两个语句确实声明了一个类型为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声明变量的初始化表达式是使用大括号括起来时,推导所得的类型就属于std::initializer_list。这么一来,如果类型推导失败(例如,大括号里的值类型不一),则代码就通不过编译。
对于大括号初始化表达式的处理方式,是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>
在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; };
resetV({1,2,3}); //错误,无法为{1,2,3}完成类型推导
要点速记
- 在一般情况下,
auto
类型推导和模板类型推导式一摸一样的,但是auto
类型推导会假定用大括号括起来的初始化表达式代表一个std::initializer_list
,但模板类型推导却不会。 - 在函数返回值或lambda式的形参中使用
auto
,意思是使用模板类型推导而非auto
类型推导。