目录
关键字auto在C++98中的语义是定义一个自动生命周期的变量,但因为定义的变量默认就是自动变量,因此这个关键字几乎没有人使用。于是C++标准委员会在C++11标准中改变了auto关键字的语义,使它变成一个类型占位符,允许在定义变量时不必明确写出确切的类型,让编译器在编译期间根据初始值自动推导出它的类型。这篇文章我们来解析auto自动类型推导的推导规则,以及使用auto有哪些优点,还有罗列出自C++11重新定义了auto的含义以后,在之后发布的C++14、C++17、C++20标准对auto的更新、增强的功能,以及auto有哪些使用限制。
推导规则
我们将以下面的形式来讨论:
auto var = expr;
这时auto代表了变量var的类型,除此形式之外还可以再加上一些类型修饰词,如:
const auto var = expr;
// 或者
const auto& var = expr;
这时变量var的类型是const auto或者const auto&,const也可以换成volatile修饰词,这两个称为CV修饰词,引用&也可以换成指针*,如const auto*,这时明确指出定义的是指针类型。
根据上面定义的形式,根据“=”左边auto的修饰情况分为三种情形:
- 规则一:只有auto的情况,既非引用也非指针,表示按值初始化
如下的定义:
auto i = 1; // i为int
auto d = 1.0; // d为double
变量i将被推导为int类型,变量d将被推导为double类型,这时是根据“=”右边的表达式的值来推导出auto的类型,并将它们的值复制到左边的变量i和d中,因为是将右边expr表达式的值复制到左边变量中,所以右边表达式的CV(const和volatile)属性将会被忽略掉,如下的代码:
const int ci = 1;
auto i = ci; // i为int
尽管ci是有const修饰的常量,但是变量i的类型是int类型,而非const int,因为此时i拷贝了ci的值,i和ci是两个不相关的变量,分别有不同的存储空间,变量ci不可修改的属性不代表变量i也不可修改。
当使用auto在同一条语句中定义多个变量时,变量的初始值的类型必须要统一,否则将无法推导出类型而导致编译错误:
auto i = 1, j = 2; // i和j都为int
auto i = 1, j = 2.0; // 编译错误,i为int,j为double
- 规则二:形式如auto&或auto*,表示定义引用或者指针
当定义变量时使用如auto&或auto*的类型修饰,表示定义的是一个引用类型或者指针类型,这时右边的expr的CV属性将不能被忽略,如下的定义:
int x = 1;
const int cx = x;
const int& rx = x;
auto& i = x; // (1) i为int&
auto& ci = cx; // (2) ci为const int&
auto* pi = ℞ // (3) pi为const int*
(1)语句中auto被推导为int,因此i的类型为int&。
(2)语句中auto被推导为const int,ci的类型为const int &,因为ci是对cx的引用,而cx是一个const修饰的常量,因此对它的引用也必须是常量引用。
(3)语句中的auto被推导为const int,pi的类型为const int*,rx的const属性将得到保留。
除了下面即将要讲到的第三种情况外,auto都不会推导出结果是引用的类型,如果要定义为引用类型,就要像上面那样明确地写出来,但是auto可以推导出来是指针类型,也就是说就算没有明确写出auto*,如果expr的类型是指针类型的话,auto则会被推导为指针类型,这时expr的const属性也会得到保留,如下的例子:
int i = 1;
auto pi = &i; // pi为int*
const char word[] = "Hello world!";
auto str = word; // str为const char*
pi被推导出来的类型为int*,而str被推导出来的类型为const char*。
- 规则三:形式如auto&&,表示万能引用
当以auto&&的形式出现时,它表示的是万能引用而非右值引用,这时将视expr的类型分为两种情况,如果expr是个左值,那么它推导出来的结果是一个左值引用,这也是auto被推导为引用类型的唯一情形。而如果expr是个右值,那么将依据上面的第一种情形的规则。如下的例子:
int x = 1;
const int cx = x;
auto&& ref1 = x; // (1) ref1为int&
auto&& ref2 = cx; // (2) ref2为const int&
auto&& ref3 = 2; // (3) ref3为int&&
(1)语句中x的类型是int且是左值,所以ref1的类型被推导为int&。
(2)语句中的cx类型是const int且是左值,因此ref2的类型被推导为const int&。
(3)语句中右侧的2是一个右值且类型为int,所以ref3的类型被推导为int&&。
上面根据“=”左侧的auto的形式归纳讨论了三种情形下的推导规则,接下来根据“=”右侧的expr的不同情况来讨论推导规则:
- expr是一个引用
如果expr是一个引用,那么它的引用属性将被忽略,因为我们使用的是它引用的对象,而非这个引用本身,然后再根据上面的三种推导规则来推导,如下的定义:
int x = 1;
int &rx = x;
const int &crx = x;
auto i = rx; // (1) i为int
auto j = crx; // (2) j为int
auto& ri = crx; // (3) ri为const int&
(1)语句中rx虽然是个引用,但是这里是使用它引用的对象的值,所以根据上面的第一条规则,这里i被推导为int类型。
(2)语句中的crx是个常量引用,它和(1)语句的情况一样,这里只是复制它所引用的对象的值,它的const属性跟变量j没有关系,所以变量j的类型为int。
(3)语句里的ri的类型修饰是auto&,所以应用上面的第二条规则,它是一个引用类型,而且crx的const属性将得到保留,因此ri的类型推导为const int&。
- expr是初始化列表
当expr是一个初始化列表时,分为两种情况而定:
auto var = {}; // (1)
// 或者
auto var{}; // (2)
当使用第一种方式时,var将被推导为initializer_list<T>类型,这时无论花括号内是单个元素还是多个元素,都是推导为initializer_list<T>类型,而且如果是多个元素,每个元素的类型都必须要相同,否则将编译错误,如下例子:
auto x1 = {1, 2, 3, 4}; // x1为initializer_list<int>
auto x2 = {1, 2, 3, 4.0}; // 编译错误
x1的类型为initializer_list<int>,这里将经过两次类型推导,第一次是将x1推导为initializer_list<T>类型,第二次利用花括号内的元素推导出元素的类型T为int类型。x2的定义将会引起编译错误,因为x2虽然推导为initializer_list<T>类型,但是在推导T的类型时,里面的元素的类型不统一,导致无法推导出T的类型,引起编译错误。
当使用第二种方式时,var的类型被推导为花括号内元素的类型,花括号内必须为单元素,如下:
auto x1{1}; // x1为int
auto x2{1.0}; // x2为double
x1的类型推导为int,x2的类型推导为double。这种形式下花括号内必须为单元素,如果有多个元素将会编译错误,如:
auto x3{1, 2}; // 编译错误
这个将导致编译错误:error: initializer for variable 'x3' with type 'auto' contains multiple expressions。
- expr是数组或者函数
数组在某些情况会退化成一个指向数组首元素的指针,但其实数组类型和指针类型并不相同,如下的定义:
const char name[] = "My Name";
const char* str = name;
数组name的类型是const char[8],而str的类型为const char*,在某些语义下它们可以互换,如在第一种规则下,expr是数组时,数组将退化为指针类型,如下:
const char name[] = "My Name";
auto str = name; // str为const char*
str被推导为const char*类型,尽管name的类型为const char[8]。
但如果定义变量的形式是引用的话,根据上面的第二种规则,它将被推导为数组原本的类型:
const char name[] = "My Name";
auto& str = name; // str为const char (&)[8]
这时auto被推导为const char [8],str是一个指向数组的引用,类型为const char (&)[8]。
当expr是函数时,它的规则和数组的情况类似,按值初始化时将退化为函数指针,如为引用时将为函数的引用,如下例子:
void func(int, double) {}
auto f1 = func; // f1为void (*)(int, double)
auto& f2 = func; // f2为void (&)(int, double)
f1的类型推导出来为void (*)(int, double),f2的类型推导出来为void (&)(int, double)。
- expr是条件表达式语句
当expr是一个条件表达式语句时,条件表达式根据条件可能返回不同类型的值,这时编译器将会使用更大范围的类型来作为推导结果的类型,如:
auto i = condition ? 1 : 2.0; // i为double
无论condition的结果是true还是false,i的类型都将被推导为double类型。
(未完待续。。。敬请点击左下角的关注以获得及时更新)
本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“AI与编程之窗”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。