C++11 の 类型推导(Auto & Decltype)


C++11引入了 autodecltype 这两个关键字,从而可以在编译期就推导出变量或者表达式的类型,方便开发者编码也简化了代码。

auto

关键字 auto 的作用便是自动推导变量/函数(C++14起)/模板(C++20起)的类型,例如这样推导是可以的:

auto foobar = 0;           // OK! 此时 auto 被编译器推导为 int
auto foo = 1, *bar = &foo; // OK! foo, bar 分别为 int 与 int*,此时 auto 被推导为 int(若将 * 删去则报错)

但不能这样推导:

auto foobar;               // ERROR! 必须初始化,否则编译器无法推导。毕竟 auto 只是一个占位符,不能真正代替数据类型进行声明
auto foo = 0, bar = 1.0;   // ERROR! foo, bar 分别为 int 和 double 类型,auto 会产生二义性,非良构

推导规则

除了上面讲的外,关于 auto 还有以下几条规则:

  1. 不允许在一个声明中混合 auto 的变量和函数,如 auto f() -> int, i = 0; 是错误的;

  2. 不允许用于函数形参的类型推导,如 void func(auto i); 是错误的;

  3. 在有 cv 限定符的类型推导中,若不声明为引用,auto 会忽略等号右边的 cv 限定;反之则保留。例如:

    int i = 0;
    const auto con_i = i;          // con_i 为 const int, auto 推导为 int
    
    // no reference
    {
      auto auto_i = i;             // auto_i 为 const int, auto 推导为 int
      auto auto_con_i = con_i;     // auto_con_i 为 const int, auto 推导为 int
    }
    
    // reference
    {
      auto &autoref_i = i;         // autoref_i 为 int&, auto 推导为 int&
      auto &autoref_con_i = con_i; // autoref_con_i 为 const int&, auto 推导为 const int&
    }

    其中,cv 限定符指关键字 constvolatile

  4. 不允许用作类的非静态成员变量

  5. 不允许用于推导数组类型,如 auto arr[3] = {1, 2, 3}; 是不允许的;

  6. 不允许用作模板参数,如 std::vector<auto> f{1, 2, 3}; 是不允许的,编译器会报 'auto' not allowed in template argument 错误;

应用场景

将变量声明为迭代器类型是一件非常痛苦的事,尤其是 std::unordered_map<Typename1, Typename2>::iterator it = map.begin(); 这样的语句,对本人这样的懒惰程序员而言简直是灾祸🤦‍♂️……

在有了 auto 后,就可以把上面的语句改写为 auto it = map.begin(),懒癌福音🥰

一般地,我个人认为,使用 auto的前提是不能影响代码可读性,对于一些不重要的中间变量,使用 auto 不会破坏可读性,还能大大提高 code 效率,但对于一些关键的变量,如函数返回值,或是利用到类多态特性的地方,则不建议用 auto

decltype

尽管都是在编译器进行类型推导,但与 auto 不同,decltype 根据已声明的变量或表达式推导其类型,无需初始化,例如:

int foo = 0;
decltype(foo) bar; // 由于 foo 为 int,则 decltype(foo) 被推导为 int

上面的 foo 可以为任意有类型的表达式,但不能是 void 类型。

void func(void);

decltype(func) p;   // OK! p 为 void(void) 的函数类型,注意和函数指针类型 void(*p)(void) 不同
decltype(func()) q; // ERROR!

推导规则

除了上面讲的外,关于 decltype 还有以下几条规则:

  1. 若推导对象为不被括号包裹的变量表达式或函数调用,则推导结果为该表达式声明时的类型或函数的返回值类型。例如:

    int i = 0;
    const int &j = i;
    int func();
    
    class Foo {
      public:
        double bar;
    }
    const Foo *foo;
    
    decltype(i) di = i;         // 推导结果为 int
    decltype(j) dj = j;         // 推导结果为 const int&
    decltype(i+j) k;            // 推导结果为 int
    decltype(func()) f;         // 推导结果为 int
    decltype(foo->bar) foo_bar; // 推导结果为 double

    不难发现,decltype会保留表达式的引用和 cv 限定符。

    反之,如果表达式类别为左值,则返回 T&;如果为右值,则返回 T,其中 T 为表达式类型。例如将上面的代码改为下面这样:

    decltype((i)) di = i;                    // 左值,推导结果为 int&
    decltype((j)) dj = j;                    // 左值。推导结果为 const int&
    decltype((i+j)) k;                       // 右值,推导结果为 int
    decltype((func())) f;                    // 右值,推导结果为 int
    decltype((foo->bar)) foo_bar = foo->bar; // 左值,推导结果为 const double&

  2. 若推导对象为函数名,则不允许出现有多个重载的函数,否则会产生二义性,例如:

    int func(int a);
    decltype(func) f1; // OK! func 仅有一个重载
    
    double func(double b);
    decltype(func) f2; // ERROR! func 有两个重载

应用场景

当类模板中需要根据传入的模板参数类中的成员变量来进一步确定类型的变量时,则用 decltype 是个很好的选择。常见于推导函数返回值类型的情况,例如:

template<typename T, typename U>
? func(T t, U u) {
    return t + u;
}

由于 T 和 U 不确定,故返回值类型无法判断。一个好的想法是试图用 decltype(t + u) 来代替 "?" 的位置,但这样会因为 t, u 未定义而报错。但如果与尾随返回类型相配合,则完美解决该问题:

template<typename T, typename U>
auto func(T t, U u) -> decltype(t + u) {
    return t + u;
}

尾随返回类型也是 C++11 新特性之一,就是用在这种需要根据函数形参类型判断返回值类型的场景中。


 上一篇
C++11 の 别名(Alias) C++11 の 别名(Alias)
虽然使用 auto 可以大大简化代码,但对于一些使用 dynamic_cast 的结果不能用 auto 作为占位符。将冗长的变量类型简化仍然是很头疼的一个问题,幸好 C++11 提供了用关键字 using 给类型起别名的特性,既能有效简化代
2023-01-17
下一篇 
C++ の 流(Stream) C++ の 流(Stream)
C++ 程序常用 cin , cout 进行标准 I/O(从键盘读取数据,或输出数据到屏幕),当然也可以进行文件 I/O (基于文件的数据读写)。这些行为本质上都是进行数据传输——数据向水流一样从一个地方流到另一个地方。C++11 中,将这
2023-01-14
  目录