编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非易事,有时甚至根本做不到。为了解决这个问题,C++11 标准引入了 auto 类型说明符,用它就能让编译器替我们去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如 double )不同,auto 让编译器通过初始值来推算变量的类型。
显然,auto 定义的变量必须有初始值:
// 由value1 和 value2 相加的结果可以推断出item的类型
auto item = value1 + value2
此处编译器将根据value1和value2相加的结果来推断item的类型。如果value1和value2的类型是double,这item的类型就是double,以此类推。
使用 auto 也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样。
auto i = 0, *p = &i; // 正确:i是整数,p是整形指针
auto sz = 0, pi = 3.14 // 错误,sz和pi的类型不一致
编译器推断出来的auto类型有时和初始值的类型并不一样,编译器会适当地改变结果类型使其更符合初始化规则。
当引用被当做初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto的类型:
int i = 0, &r = i;
auto a = r; // a是一个整数(r是i的别名,而i是一个整数)
auto一般会忽略掉顶层的const,同时底层const则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci = i, &cr = ci;
auto b = ci; // b是一个整数,ci的顶层const特性被忽略掉了
auto c = cr; // c是一个整数,cr是ci的别名,ci本身是一个顶层const
auto d = &i; // d是一个整形指针
auto e = &ci; // e是一个指向整数常量的指针,对常量对象取地址是一种底层const
如果希望推断出的auto类型是一个顶层const,需要明确指出:
const auto f = ci;
还可以将引用的类型设为auto,此时原来的初始化规则仍然适用:
auto &g = ci; // g是一个整型常量引用,绑定到ci
auto &h = 42; // 错误,不能为非常量引用绑定字面值
const auto &j = 42; // 正确,可以为常量引用绑定字面值
当使用条件表达式初始化auto声明的变量时,编译器总是使用表达能力更强的类型:
auto i = true ? 5 : 8.0; // i的数据类型为double
在上面的代码中,虽然能够确定表达式返回的是int类型,但是i的类型依旧会被推导为表达能力更强的类型double。
幸运的是,在C++17标准中,对于静态成员变量,auto可以
在没有const的情况下使用,例如:
struct sometype {
static auto i = 5; // C++17
};
按照C++20之前的标准,无法在函数形参列表中使用auto声明
形参(注意,在C++14中,auto可以为lambda表达式声明形参):
void echo(auto str) {…} // C++20之前编译失败,C++20编译成功
使用auto声明变量,如果目标对象是一个数组或者函数,则auto会被推导为对应的指针类型:
int i[5];
auto m = i; // auto推导类型为int*
int sum(int a1, int a2)
{
return a1+a2;
}
auto j = sum // auto推导类型为int (__cdecl *)(int,int)
当auto关键字与列表初始化组合时,这里的规则有新老两个版本,这里只介绍新规则(C++17标准)。
直接使用列表初始化,列表中必须为单元素,否则无法编译,auto类型被推导为单元素的类型。
用等号加列表初始化,列表中可以包含单个或者多个元素,auto类型被推导为std::initializer_list,其中T是元素类型。请注意,在列表中包含多个元素的时候,元素的类型必须相同,否则编译器会报错
auto x1 = { 1, 2 }; // x1类型为 std::initializer_list<int>
auto x2 = { 1, 2.0 }; // 编译失败,花括号中元素类型不同
auto x3{ 1, 2 }; // 编译失败,不是单个元素
auto x4 = { 3 }; // x4类型为std::initializer_list<int>
auto x5{ 3 }; // x5类型为int
合理使用auto,可以让程序员从复杂的类型编码中解放出来,不但可以少敲很多代码,也会大大提高代码的可读性。但是事情总是有它的两面性,如果滥用auto,则会让代码失去可读性,不仅让后来人难以理解,间隔时间长了可能自己写的代码也要研读很久才能弄明白其含义。所以,如何合理地使用auto是需要有一定的规则的。当然了,每个人可能会有自己的理解。
当一眼就能看出声明变量的初始化类型的时候可以使用auto。
如果使用auto声明变量,则会导致其他程序员阅读代码时需要翻阅初始化变量的具体类型,那么我们需要慎重考虑是否适合使用auto关键字。
对于复杂的类型,例如lambda表达式、bind等直接使用auto。
我们有时候会遇到无法写出类型或者过于复杂的类型,或者即使能正确写出某些复杂类型,但是其他程序员阅读起来也很费劲,这种时候建议使用auto来声明,例如lambda表达式
C++14标准支持对返回类型声明为auto的推导,例如:
auto sum(int a1, int a2) { return a1 + a2; }
在上面的代码中,编译器会帮助我们推导sum的返回值,由于a1和a2都是int类型,所以其返回类型也是int,于是返回类型被推导为int类型。
请注意,如果有多重返回值,那么需要保证返回值类型是相同的。例如以下代码会编译失败:
auto sum(long a1, long a2)
{
if (a1 < 0) {
return 0; // 返回int类型
}else {
return a1 + a2; // 返回long类型
}
}
以上代码中有两处返回,return 0返回的是int类型,而return a1+a2返回的是long类型,这种不同的返回类型会导致编译失败。
在C++14标准中我们还可以把auto写到lambda表达式的形参中,这样就得到了一个泛型的lambda表达式,例如:
auto l = [](auto a1, auto a2) { return a1 + a2; };
auto retval = l(5, 5.0);
在上面的代码中a1被推导为int类型,a2被推导为double类型,返回值retval被推导为double类型。
lambda表达式返回auto引用的方法:
auto l = [](int &i)->auto& { return i; };
auto x1 = 5;
auto &x2 = l(x1);
assert(&x1 == &x2); // 有相同的内存地址
起初在后置返回类型中使用auto是不允许的,但是后来人们发现,这是唯一让lambda表达式通过推导返回引用类型的方法了。
C++17标准对auto关键字又一次进行了扩展,使它可以作为非类型模板形参的占位符。当然,我们必须保证推导出来的类型是可以用作模板形参的,否则无法通过编译,例如
#include <iostream>
template<auto N> // N 是一个非类型的模板参数
void f()
{
std::cout << N << std::endl;
}
int main()
{
f<5>(); // N为int类型
f<'c'>(); // N为char类型
f<5.0>(); // 编译失败,模板参数不能为double(不过在MSVC C++20下可以编译通过,可能有特殊处理)
}
在上面的代码中,函数f<5>()中5的类型为int,所以auto被推导为int类型。同理,f<‘c’>()的auto被推导为char类型。由于f<5.0>()的5.0被推导为double类型,但是模板参数不能为double类型,因此导致编译失败。
至于double类型为什么不行,这就要牵扯到non-type template parameter本身的限制了。
关于 non-type template parameter的一点扩展:
a non-type template-parameter shall have one of the following types(无类型模板参数应该是下面所列举的类型):
参考文献: