现代C++语言核心特性解析
上QQ阅读APP看书,第一时间看更新

第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;
}

在上面的代码中,我们不需要为istr去声明具体的类型,auto要求编译器自动完成变量类型的推导工作。sum函数中的auto是一个返回值占位符,真正的返回值类型是intsum函数声明采用了函数返回类型后置的方法,该方法主要用于函数模板的返回值推导(见第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 *推导ij的类型。我不建议像上面这样使用auto,因为它会破坏代码的可读性。在后面的内容中,我们将讨论应该在什么时候避免使用auto关键字。