Item 2:Understand auto type deduction
如果你已经读过Item1中的模板类型推断,那么你已经知道几乎所有的你需要知道的关于auto类型推断的知识了。除了仅有的一个例外,auto类型推断就是模板类型推断。但它是如何做到的呢?模板类型推断含有模板和函数和参数,但是auto处理的东西并不包含这些。
那是事实,但是却不影响。模板类型推断和auto类型推断之间有着直接的对应关系。他们之间是可以逐字转换的。
在Item1中,解释模板类型推断的时候用的是下面这种一般的模板函数
template<typename T>
void f(ParamType param);
和这样的调用:
f(expr);
在调用f时,编译器用expr去推断T和ParamType的类型。
当一个变量用auto进行声明时,auto的作用和模板中T的作用相同,类型说明符和ParamType的作用相同。展示他们比描述他们容易一点,所以考虑下面的例子:
auto x = 27;
这里,x的类型说明符就单是auto自己。再看一个例子,在这个声明中
const auto cx = x;
类型说明符是const auto。再看一个
const auto& rx = x;
类型说明符是const auto&。在推断x,cx和rx的类型时,编译器的表现就像是模板函数以及调用这个模板函数且传入对应的初始化表达式:
template<typename T>
void func_for_x(T param);
func_for_x(27); //param的类型被推断为x的类型
template<typename T>
void func_for_cx(const T param);
func_for_cx(x); //param的类型被推断为cx的类型
template<typename T>
void func_for_rx(const T& param);
func_for_rx(x); //param的类型被推断为rx的类型
就象我说的那样,auto的类型推断除了仅有的一个例外(我们马上会讨论),其余的和模板类型推断是相同的。
Item1中将模板类型推断分为三种情况,根据的是ParamType的特点,即模板函数中param的类型说明符。用auto进行变量声明的情况下,类型说明符代替了ParamType的位置,所以我们也根据它分为三种情况:
case1:类型说明符是指针或者引用,但不是universal引用。
csae2:类型说明符是universal引用。
case3:类型说明符既不是指针也不是引用。
/*这里的例子与Item1中几乎无差别,故先跳过,以后有时间补充。*/
就像你看到的那样,auto类型推断与模板类型推断是类似的,就其本质来说是一枚硬币的两面。
考虑两种方法有所区别的例外情况。我们从你用初始值27声明一个int值开始,c++98给你两种语法上的选择:
int x1 = 27;
int x2(27);
c++11,根据它对统一初始化(uniform initialization)的支持,增加了下面这些方法:
int x3 = { 27 };
int x4{ 27 };
总共有四种形式,但只产生一种结果:一个具有值27的int变量。
但是Item5中解释道,用auto声明变量比确定类型声明变量更有好处,所以上面的例子中用auto代替int是不错的选择。直接在文本上替换产生如下代码:
auto x1 = 27; //类型是int,值是27
auto x2(27); //同上
auto x3 = { 27 }; //类型是std::initializer_list<int>,值是{ 27 }
auto x4{ 27 }; //同上
这些声明都能通过编译,但是与他们代替的代码相比,他们并不拥有相同的含义。前两种声明用27声明了int型变量。后两种声明了std::initializer_list<int>型变量,该变量只包含单个元素27!
这是因为auto的一个特殊的类型推断规则。当大括号初始化一个auto声明的变量时,推断的类型将会是std::initializer_list。如果这种类型不能被推断出来(因为大括号初始化中有不同的类型),代码将通不过编译。
auto x5 = { 1,2,3.0}; //error!无法推断T。
向上述说明的那样,这个例子中的类型推断将会失败,但是认识到这里实际上有两种类型推断产生是很重要的。其中一种产生于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);
f({11,23,9}); //error!无法推断T的类型!
然而,如果你对于未知类型T定义模板中的param是std::initializer_list<T>,模板类型推断将会推断出T的类型。
template<typename T>
void f(std::initializer_list<T> initList);
f({11,23,9}); //T被推断为int,initList的类型是std::initializer_list<int>
所以auto和模板类型推断的唯一不同是auto假设大括号初始化代表了std::initializer_list,但模板类型推断不会。
你也许想知道为什么auto类型推断对于大括号初始化有这样的特别规则,但是模板初始化却没有。我没有找到一个具有说服力的答案。但是规则就是规则,这意味着你必须记住它,如果你用auto声明一个变量且你用大括号初始化它,类型推断将总会是std::initializer_list。如果你接受统一初始化的哲学理念--将初始值包括在大括号里是必须的,那么将这种情况记在脑子里是很重要的。c++11程序开发中中一个典型的失误就是偶然声明了一个std::initializer_list的变量,但你想要一个其他的类型时。这个陷阱就是一些开发者只有当他们不得不用大括号初始化时才使用它的一个原因。(Item7中讨论了什么时候你必须使用它)
对于c++11,这就是全部的故事了,但是对于c++14,故事还在继续。c++14允许auto作为函数的应该被推断的返回类型(看Item3),并且c++14的lambda在参数声明时也可能使用auto。然而,auto的这种使用用的是模板类型推断的规则,并不是auto类型推断的规则。所以,一个具有auto返回类型的函数返回一个大括号初始化列表是不通过编译的:
auto createInitLiat()
{
return { 1, 2, 3}; //error!不能对{ 1, 2, 3 }进行类型推断
}
当auto被用于c++14lambda的参数类型声明时这种情况同样会发生:
std::vector<int> v;
...
auto resetV =
[&v](const auto& newValue) { v = newValue; };
...
resetV({ 1, 2, 3 }); //error!不能对{ 1, 2, 3 }进行类型推断
Things to Remember
1.通常来说auto类型推断和模板类型推断是一样的,但是auto类型推断假设大括号初始化代表着std::initializer_list,而模板类型推断并不会。
2.auto在函数返回类型或是lambda参数中应用模板类型推断的规则,并不应用auto类型推断规则。