在现代C++编程中,自动类型推导是非常重要且使用非常广泛的特性之一。在新的C++标准中,可以在任何地方使用auto作为类型的占位符,编译器会自动推导出它的实际类型。在C++11中,auto可以用于声明局部变量和尾部返回值指定类型的函数。在C++14中,auto可以用于没有指定尾部返回类型的函数,同时也可以用于lambda表达式中参数的声明。未来的标准可能会扩展auto,使之适用于更多的场景。使用auto关键字的几个主要的好处将在1.1.2节讨论。开发者应该意识到这一点并尽可能地使用auto关键字。为此,Andrei Alexandrescu专门创造了一个术语,并由Herb Sutter进行了推广——AAA(almost always auto)。
以下情形可以考虑使用auto作为实际类型的占位符:
❍ 当不想指定特定类型时,可以使用auto name=expression形式声明局部变量:
❍ 当需要指定类型时,可以使用auto name=type-id {expression}形式声明局部变量:
❍ 声明命名lambda函数时,格式为auto name=lambda-expression,除非lambda需要作为某个函数的参数或返回值:
❍ 声明lambda表达式的参数和返回值:
❍ 当不想指定函数返回值类型时,用于声明函数返回类型:
auto标识符基本上可以说是实际类型的占位符。使用auto时,编译器会从以下情况推导出实际类型:
❍ 当auto用于声明变量时,从用于初始化变量的表达式类型推导。
❍ 当auto用作函数返回类型的占位符时,从函数的尾部返回类型或者函数返回的表达式推导。
有时,我们必须指定类型。例如,在1.1.1节的第一个例子中,变量s被编译器推导为char const *类型。如果目标是使用std::string,那么就必须显式地指定变量s的类型(std::string)。同样,v的类型被推导为std::initializer_list<int>。然而,你可能需要的是std::vector<int>类型,在这种情况下,必须在赋值符的右边显式指定类型。
用auto标识符来代替真正的类型有很多好处,下面列出了重要的几条:
❍ 不会造成变量未初始化。这是开发者最常见的一个错误(声明没有初始化的特定类型变量)。使用auto,这种情况不可能发生,因为编译器为了能够正确推导,要求必须对变量初始化。
❍ 使用auto可以保证始终使用正确的类型,同时保证不会发生隐式转换。考虑下面这个通过局部变量获取vector的大小的例子。在第一个例子中,变量的类型是int类型,即使size()方法的返回值类型是size_t。这就意味着发生了从size_t到int类型的隐式转换。但是,用auto关键字就可以推导出正确的类型,即size_t类型。
❍ 使用auto有利于推动良好的面向对象编程实践,例如更倾向于采用接口而不是具体实现。指定的类型越少,代码的通用性越强,并且对未来越开放,这是面向对象编程的一个基本原则 。
❍ 使用auto可以精简代码,同时无须过多关心真正的数据类型。一个非常常见的现象是虽然我们指定了类型,但是我们其实很少关注类型。虽然这种现象在使用迭代器的场景中很常见,但是还有很多场景也是这样。当想迭代一个可迭代对象时,你根本就不会关心迭代器的类型,你关心的只有迭代器本身。所以,使用auto不但可以节省时间(因为迭代器的名字有可能会很长),而且还可以让你把精力集中在业务代码上而不是类型上。在下面的例子中,第一个for循环显式地使用了迭代器的类型,迭代器的类型名很长,长语句代码会降低代码的可读性,而且你还必须要清楚实际并不关心的迭代器的类型。第二个for循环用到了auto标识符,看起来很简练,不仅可以节约敲代码的时间,而且你只需要关注迭代器本身:
❍ 使用auto声明变量提供了一致的编码风格,类型始终位于右侧。如果要动态分配对象,需要在赋值符的左右两边都写出类型,例如int* p=new int(42)。但是,使用auto,类型只需在右侧指定一次。
然而,使用auto有一些需要注意的问题:
❍ auto标识符只是类型的占位符,不能用于const/volatile以及引用类型。如果需要const/volatile以及引用类型,则需要显式指定它们。在下面的例子中,foo.get()返回的是一个int类型的引用。当变量x根据函数返回值进行初始化时,编译器推导的结果是int类型而非int&。因此,对x所做的任何修改都不会改变foo.x_。为此,我们应该使用auto&。
❍ 不能对不可移动的类型使用auto标识符:
❍ auto标识符不能用于多字类型,例如long long、long double或者struct foo。但是,对于第一种情况,可能的解决方法是使用字面量或类型别名;对于第二种情况(struct foo),使用struct/class这种形式只是为了使C++与C语言保持兼容,这种情况无论如何都应该避免:
❍ 如果使用auto标识符但同时也想知道类型,那么只需要把鼠标放在变量上面即可,因为大多数IDE都支持这种操作。当然,离开IDE,这种操作就失效了,只能自己通过初始化表达式推导真正的类型,这意味着可能要搜索代码,因为表达式可能是一个函数调用。
auto标识符可用于指定函数的返回类型。在C++11标准中,在函数声明时需要给出尾部返回类型。但是在C++14中就没有这个限制了,返回值的类型通过return表达式自动推导。如果有多个返回值,它们应该具有相同的类型:
如前所述,auto不保留const/volatile和引用限定符。这会导致auto作为函数返回类型的占位符出现问题。为了解释这一点,请考虑前面的foo.get()示例。这次,我们有一个名为proxy_get()的包装函数,它接受一个对foo的引用,调用get()并返回get()返回的值,即int&。然而,编译器会将proxy_get()的返回类型推导为int,而不是int&。尝试将int值赋给int&类型会失败,从而引发错误:
为了解决这个问题,我们需要指定函数返回auto&。然而,这对于模板来说是一个问题,因为模板在完美转发返回值时无法判断返回类型是值还是引用。C++14对于这个问题的解决方法是使用decltype(auto),这可以确保类型推导的正确性:
decltype标识符用于检查实体或者表达式的声明类型。当声明类型很麻烦或根本不可能用标准表示法声明时,它很有用。这样的例子包括lambda类型和依赖模板参数的类型。
最后一个使用auto标识符的重要场景是lambda表达式。从C++14开始,lambda返回类型和参数类型都可以使用auto。这样的lambda被称为泛型lambda,因为由lambda定义的闭包类型具有模板调用运算符的功能。下面的例子展示了一个泛型lambda,它接受两个auto类型的参数并返回operator+运算后的结果:
这样的lambda可用于将定义了operator+的任何对象相加,如以下代码片段所示:
在这个例子中,我们将ladd lambda用于两个整数相加以及两个字符串(std:string)对象拼接(字符串运用了C++14中定义的字面量运算符""s)。
❍ 阅读1.2节,以了解类型的别名。
❍ 阅读1.3节,以了解花括号初始化的工作原理。