弄清auto类型推断
如果你已经读了Item 1关于模板类型推断的内容,你现在差不多已经知道了关于auto类型推断的所有知识,因为,除了有一个比较奇怪的例外,auto类型推断就是模板类型推断。但是这又是如何做到的?模板类型推断涉及到模板、函数以及参数,但是auto却没有处理这其中任何一个。
事实虽然如此,但是这并不影响。模板类型推断和auto类型推断之间有一个直接映射关系,有一个算法可以将其中一个转换成另外一个。
在Item 1里面,我们用了如下的通用函数模板来解释模板类型推断
template<typename T>
void f(ParamType param);
还有这通用的调用形式:
f(expr); //call f with some expression
在对f的调用中,编译器使用expr来推断T和ParamType。
当一个变量使用auto来进行声明,那么auto其实扮演了模板中T的角色,而变量的类型修饰符(type specifier)则扮演了ParamType的角色。展示其实比描述要简单,看看下面的例子:
auto x = 27;
这里,x的类型修饰符就是auto自身。另一方面,在声明式:
const auto cx = x;
类型修饰符就是const auto了。这里,
const auto& rx = x;
类型修饰符是const auto&。为了给这些例子中的x,cx,rx推断出对应的类型,编译器表现的就像是对每一个声明都有一个模板并且有一个根据对应的初始化表达式对于该模板的调用:
template<typename T> // conceptual template for
void func_for_x(T param); // deducing x's type
func_for_x(27); // conceptual call: param's
// deduced type is x's type
template<typename T> // conceptual template for
void func_for_cx(const T param); // deducing cx's type
func_for_cx(x); // conceptual call: param's
// deduced type is cx's type
template<typename T> // conceptual template for
void func_for_rx(const T& param); // deducing rx's type
func_for_rx(x); // conceptual call: param's
// deduced type is rx's type
正如我所说,对于auto的类型推断,除了有一点例外(我们后面会介绍),其他跟模板类型推断一模一样。
Item 1基于通用函数模板中ParamType的特性,也就是param的类型修饰符,将模板类型推断分成了3个不同的case。而在使用auto的变量声明中,类型修饰符取代了ParamType,所以也有3种不同的case:
- Case 1: 类型修饰符是一个指针或者引用,但不是一个universal引用
- Case 2: 类型修饰符是一个universal引用
- Case 3: 类型修饰符既不是指针也不是引用
我们在上面的例子中已经见识过Case 1和Case 3:
auto x = 27; //case 3(x is neither ptr nor reference)
const auto cx = x; // case 3(cx isn't either)
const auto& rx = x; //case 1(rx is a non-universal ref.)
而Case 2的应用结果也正如你所料:
auto&& uref1 = x;//x is int and lvalue, so uref1's type is int&
auto&& unref2 = cx;//cx is const int and lvalue,so uref2's type is const int&
auto&& uref3 = 27; // 27 is int and rvalue,so uref3's type is int&&
Item 1归纳了对于非引用类型的数组和函数名字是如何退化成指针的,这对于auto的类型推断也同样适用:
const chat name[] = "R. N. Briggs"; //name's type is const char[13]
auto arr1 = name ; //arr1's type is const char*
auto& arr2 = name; //arr2's type is const char (&)[13]
void someFunc(int,double); //someFunc is a function,type is void(int,double)
auto func1 = someFunc; //func1's type is void (*)(int,double)
auto& func2 = someFunc; //func2's type is void (&)(int,double)
正如你所看到,auto类型推断工作原理和模板类型推断基本上一样。它们就像是相同硬币的两面。
但是除了有一点它们之间有差别的地方。我们从观察声明一个int类型的变量并初始化为27开始,C++98给你两种语法选择:
int x1 =27;
int x2(27);
而C++11,通过其对统一初始化(uniform initialization )的支持,增加了这些:
int x3 = {27};
int x4{27};
总而言之,这四种语法都只有一个结果:一个值为27的int
但是正如Item 5会解释的那样,用auto进行声明比用固定类型来声明更加有好处,所以将上面变量声明中的int换成auto应该更好。直观的文本替换产生下面的代码:
auto x1 = 27;
auto x2(27);
auto x3 = {27};
auto x4{27};
这些声明全都可以通过编译,但是他们并不全和他们替换掉的声明有着相同的意思。前两个的确声明了值为27的int变量,但是后面两个却声明了std::initializer_list类型,其仅包含一个值为27的元素!
auto x1 = 27; //type is int,value is 27
auto x2(27); //ditto
auto x3 = {27}; // type is std::initializer_list<int>, value is {27}
auto x4{27}; //ditto
这是由auto的特殊类型推断规则造成的。当一个用auto声明的变量的初始化被括起来时,推断出的类型就是std::initializer_list类型了。如果这么一个类型不能被推断出(比如,被括起来的初始化值不是一个类型的),代码将通不过编译:
auto x5 = {1,2,3.0}; //error! can't deduce T for std::initializer_list<T>
正如评论所说的,类型推断在这种情况下会失败,但是很重要的一点是我们需要知道这里面其实包含了两种类型推断。一种是auto:x5的类型需要被推断出。由于x5的初始化值都在括号内,x5必须被推断成std::initializer_list类型。但是std::initializer_list是一个模板。其实例化是对某种类型T的std::initializer_list,这也就意味着类型T也需要被推断出。上面的错误发生在第二种类型推断:模板类型推断。在该例子中,推断出错,因为初始化值并不是一个类型。
对于括号初始化的区别对待是auto类型推断和模板类型推断唯一不一样的地方。当一个auto声明的变量被用括号初始化后,其推断类型是一个std::initilializer_list的实例。但是如果将相同的初始化值传递给对应的模板,类型推断就出错,代码也通不过编译:
auto x = {11,23,9}; // x's type is std::initializer_list<int>
template<typename T> //template with parameter declaration equivalent to x's declaration
void f(T param);
f({11,23,9}); //error!can't deduce type for T
然而,如果指定模板的param是针对某位置类型T的std::initializer_list类型,那么模板类型推断会推断出T的类型:
tmeplate<typename T>
void f(std::initializer_list<T> initList);
f({11,23,9}); //T deduced as int, and initList's type is std::initializer_list<int>
所以auto和模板类型推断之间的真正区别就在于auto假设用括号初始化的值代表了std::initializer_list类型,但是模板却不这么假设。
你也许会迷惑为什么auto类型推断会针对括号初始化有一个特殊规则,而模板类推断则没有。其实我自己也有这个疑惑。哎,我到现在都没有找到一个令人信服的解释。但是规则总归是规则,这就意味着你必须记住:当你使用auto来声明变量并用括号初始化它的时候,它的推断类型就是std::initializer_list类型了。如果你拥抱了统一初始化哲学–就是将初始化值括起来,那么记住上面那一点就尤为重要。在C++11编程中一个经典错误就是你不小心声明了一个std::initializer_list类型变量,但是你明明是想声明一个其他类型的。这种陷阱也是很多开发者只有在必须这么做的时侯才将他们的初始化值括起来的愿意之一。(什么时候必须这么做会在Itm 7中讨论)
对于C++11,这就是所有的故事,但是对于C++14,传说还在继续。C++14允许auto来指示(编译器):函数类型也应该被推断出(请参考Item 3),并且C++14 lambdas可以在参数声明中使用auto。然而,这些对于auto的使用套用的是模板类型推断规则,并不是auto的类型推断规则。所以当一个函数使用auto来返回一个括号初始化的值时,就不会通过编译:
auto createInitList()
{
return {1,2,3}; //error:can't deduce type for {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}); //error!can't deduce type for {1,2,3}
记忆要点
- auto类型推断一般和模板类型推断一样,但是auto类型推断假设括起来的初始化值表示std::initializer_list类型,但模板类型推断就不会。
- 对函数的返回类型或者lambda表达式的参数使用auto时,其应用的是模板类型推断规则,而不是auto的类型推断规则。