C++20标准下的auto类型指示符

一、auto类型说明符简介

编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非易事,有时甚至根本做不到。为了解决这个问题,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类型有时和初始值的类型并不一样,编译器会适当地改变结果类型使其更符合初始化规则。

当引用被当做初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为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声明变量,则会导致其他程序员阅读代码时需要翻阅初始化变量的具体类型,那么我们需要慎重考虑是否适合使用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类型,这种不同的返回类型会导致编译失败。

五、lambda表达式中使用auto类型推导

在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表达式通过推导返回引用类型的方法了。

五、非类型模板参数使用auto

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(无类型模板参数应该是下面所列举的类型):

  1. integral or enumeration type(整型 或者 枚举)
  2. pointer to object or pointer to function(对象的指针或函数指针,其实还包括基本类型的指针)
  3. reference to object (对象的引用或者函数的引用)
  4. Attention :The C++ standard does not allow floating point non-type template parameters
    (注意:c++标准规定浮点型不能作为无类型模板的参数 例如 float,double,但他们的引用和指针是允许的。)

参考文献:

  1. 《C++ Primer第五版》
  2. 《现代C++语言核心特性解析》

猜你喜欢

转载自blog.csdn.net/hubing_hust/article/details/128606898