第3章 auto占位符(C++11~C++17)
3.1 重新定义的auto关键字
严格来说auto
并不是一个新的关键字,因为它从C++98标准开始就已经存在了。当时auto
是用来声明自动变量的,简单地说,就是拥有自动生命期的变量,显然这是多余的,现在我们几乎不会使用它。于是C++11标准赋予了auto
新的含义:声明变量时根据初始化表达式自动推断该变量的类型、声明函数时函数返回值的占位符。例如:
auto i = 5; // 推断为int
auto str = "hello auto"; // 推断为const char*
auto sum(int a1, int a2)->int // 返回类型后置,auto为返回值占位符
{
return a1+a2;
}
在上面的代码中,我们不需要为i
和str
去声明具体的类型,auto
要求编译器自动完成变量类型的推导工作。sum
函数中的auto
是一个返回值占位符,真正的返回值类型是int
,sum
函数声明采用了函数返回类型后置的方法,该方法主要用于函数模板的返回值推导(见第5章)。注意,auto
占位符会让编译器去推导变量类型,如果我们编写的代码让编译器无法进行推导,那么使用auto
会导致编译失败,例如:
auto i; // 编译失败
i = 5;
很明显,以上代码在声明变量时没有对变量进行初始化,这使编译器无法确认其具体类型要导致编译错误,所以在使用auto
占位符声明变量的时候必须初始化变量。进一步来说,有4点需要引起注意。
1.当用一个auto
关键字声明多个变量的时候,编译器遵从由左往右的推导规则,以最左边的表达式推断auto
的具体类型:
int n = 5;
auto *pn = &n, m = 10;
在上面的代码中,因为&n
类型为int *
,所以pn
的类型被推导为int *
,auto
被推导为int
,于是m
被声明为int
类型,可以编译成功。但是如果写成下面的代码,将无法通过编译:
int n = 5;
auto *pn = &n, m = 10.0; // 编译失败,声明类型不统一
上面两段代码唯一的区别在于赋值m
的是浮点数,这和auto
推导类型不匹配,所以编译器通常会给予一条“in a declarator-list 'auto' must always deduce to the same type”报错信息。细心的读者可能会注意到,如果将赋值代码替换为int m = 10.0;
,则编译器会进行缩窄转换,最终结果可能会在给出一条警告信息后编译成功,而在使用auto声明变量的情况下编译器是直接报错的。
2.当使用条件表达式初始化auto
声明的变量时,编译器总是使用表达能力更强的类型:
auto i = true ? 5 : 8.0; // i的数据类型为double
在上面的代码中,虽然能够确定表达式返回的是int
类型,但是i的类型依旧会被推导为表达能力更强的类型double
。
3.虽然C++11标准已经支持在声明成员变量时初始化(见第8章),但是auto
却无法在这种情况下声明非静态成员变量:
struct sometype {
auto i = 5; // 错误,无法编译通过
};
在C++11中静态成员变量是可以用auto
声明并且初始化的,不过前提是auto
必须使用const
限定符:
struct sometype {
static const auto i = 5;
};
遗憾的是,const
限定符会导致i
常量化,显然这不是我们想要的结果。幸运的是,在C++17标准中,对于静态成员变量,auto
可以在没有const
的情况下使用,例如:
struct sometype {
static inline auto i = 5; // C++17
};
4.按照C++20之前的标准,无法在函数形参列表中使用auto
声明形参(注意,在C++14中,auto
可以为lambda表达式声明形参):
void echo(auto str) {…} // C++20之前编译失败,C++20编译成功
另外,auto
也可以和new
关键字结合。当然,我们通常不会这么用,例如:
auto i = new auto(5);
auto* j = new auto(5);
这种用法比较有趣,编译器实际上进行了两次推导,第一次是auto(5)
,auto
被推导为int
类型,于是new int
的类型为int *
,再通过int *
推导i
和j
的类型。我不建议像上面这样使用auto
,因为它会破坏代码的可读性。在后面的内容中,我们将讨论应该在什么时候避免使用auto
关键字。