C++11 新特性——类型推导

 
 

写在前面

  C++ 是有四个相关语言组成的联邦——C、object-C、template C++、STL。如果需要将这些东西全部杂糅到一起,总会出现一些抵触的情况,C++ 的编码也会变得更复杂。新标准增加了许多新特性,使得 C++ 更加易用。这些新特性是紧密相连,互为基础的。
  在C++11中,规范提供了多种类型推导的机制,使得我们写出来的代码更精简、更灵活。
  这个新的类型推导机制,主要由两个新的关键字表示:auto 和 decltype。虽然它们之间有些重叠,但使用场景是不同的。auto 只能用来声明变量,而 decltype 则更为通用。

 

auto 类型推导

  在 C++ 中,每个变量使用前必须定义几乎是天经地义的事情。像 Python、JavaScript 等动态语言不需要声明,几乎拿来就用。静态类型和动态类型的主要区别在于对变量进行类型检查的时间点。对于静态类型,类型检查主要发生在编译阶段;对于动态语言,类型检查主要发生在运行阶段。从信息的角度来说,其实编译期就能够得到定义类型的类型信息。如果能够实现一种技术:程序员使用一个变量表示类型由编译器推导,编译器在编译的时候回去类型信息,再返回将类型覆盖到变量中,就可以实现类似动态语言的能力。C++ 11 中,这个技术就叫类型推导。

int main() {
    auto name = "world.";
    std::cout << "hello, " << name <<std::endl;
}

  这里我们使用了 auto 关键字来要求编译器对变量 name 的类型进行自动推导。编译器根据它的初始化表达式类型,推导出 name 的类型为 char*。
  auto 关键字在早起 C/C++ 标准中有着完全不同的定义。声明时使用 auto 修饰的变量,按照早起 C/C++ 标准的解释,是具有自动存储期的局部变量。但是现实中几乎没有人使用,因为一般函数内没有声明为 static 变量总是具有自动存储期的局部变量。
  在 C++ 11 中,重写了 auto 的定义:auto 声明变量的类型必须由编译时期推导而得。

int main() {
    double foo();
    auto x = 1;    // x 的类型为 int
    auto y = foo();   // y 的类型为 double
    struct m { int i;} str;
    auto str1 = str;  // str1 的类型是 struct m
    auto z;    // 无法推导,无法通过编译
    z = x;
}

  auto 声明的变量必须被初始化,以使编译器能够从其初始化表达式中推导出其类型。从这个意义上来讲,auto 并非一种“类型”说明,而是一个类型声明时的“占位符”,编译器在编译时期会将 auto 替代为变量实际的类型。
  auto 推导的最大优势是在拥有初始化表达式的复杂类型变量时简化代码,比如 STL 库的各种容器的迭代器。
 

auto 推导规则

  • 对于指针类型,声明为 auto、auto* 是没有区别的。
  • 声明引用类型,必须使用 auto &。
  • 不能得到推导变量的 const、volatile 属性
int x;
int * y = &x;
double foo();
const int foo2();

auto *a = &x; // 正常推导, a 的类型为 int *
auto b = &x;  // 规则一,b 的类型为 int *
auto &c = x; // 规则二,c 的类型为 int&
auto d = foo(); // 正常推导,d 的类型为 double
auto e = foo2(); // 规则三,e 的类型为 int,不继承 const
const auto e = foo2(); // 规则三, e 的类型为 const int,手动描述 auto 为 const

  总的来说,auto 在 C++ 11 中是相当关键的特性之一。比如追踪返回类型的函数声明、lambda 和 auto 的配合使用等。auto 只是 C++ 11 类型推导的一部分,还有一部分应该使用 decltype 来体现。

decltype

  C++ 98 标准中就部分支持动态类型,使用的是运行时类型识别(RTTI)。
  RTTI 的机制是为每个类型产生一个 type_info 类型的数据,使用 typeid( * ) 返回相应变量的 type_info 数据。type_info 的 name 成员函数可以返回类型的名字,返回字符串为数字加字符的组合如:3int,6double。在 C++ 11 中增加了 hash_code 成员函数,返回该类型的哈希值,但是标准中并不保证唯一,可以使用 operator== 来判断类型相同。
  但是在实际使用中,程序员需要的是使用这样的类型而不是识别该类型,因此 RTTI 无法满足需求。C++ 因此在新标准中增加了 auto 和 decltype 关键字。

int main() {
    int i;
    decltype(i) j = 0;
    std::cout << typeid(j).name() << std::endl;   // int

    float a; double b;
    decltype(a + b) c;
    std::cout << typeid(c).name() << std::endl;  // double
}

 

decltype 推导规则

  规则推导依次进行,满足任一条件就退出推导。
– 单个标记符和类成员表达式,推导为 T。
– 右值引用,推导为 T&&。
– 非单个标记符的左值,推导为 T&。
– 不满足以上三个条件,则推导为 T。

规则一:
int a; std::string b; struct C{} c; 
decltype(c) cc; // 单个标记,类型为 struct C
规则二:
struct D {} d; D&& func1() { ... }
decltype(std::move(d)) dd = func1(); // 右值引用,推导为 D&&
decltype(func1()) ddd = std::move(d); // 右值引用,推导为 D&&
规则三:
double arr[5]
decltype(++a) aaa = a; // 两个标记 ++ 和 a,推导为 int &
decltype(arr[3]) arrv = 1.0; // 非单个标记,推导为 double &
decltype("Hello") h = "hello"; // 字符串字面值为左值,推导为 const char (&)[6]
规则四:
const bool Func2(int);
decltype(1) iii = 2; // 字符串外的字面常量为 右值,不满足任一条件,推导为 int
decltype(i++) iiii = 4; // 返回为右值,推导为 int
decltype(Func2(1)) bbb = false; // 返回为右值,推导为 const bool

  所以,对于 int i,编译器 decltype(i) 和 decltype((i)) 的编译结果不同,因为 i 是单个标记符,推导为 int;(i) 是标记符加小括号,不满足规则一,但满足规则三,推导为 int &。如果 decltype((i)) 没有给予左值 int 就会报错——左值引用未初始化。

const、volatile 继承

  decltype 与 auto 不同,decltype 是可以带走 const 和 volatile 限制符的,但是如果类的实例是使用 const 限制的,而类的成员定义没用 const 限制,则 decltype 推导的结果是不带 const 定义的,volatile 也是这样。
  decltype 也可以手动设定 const、volatile、&、&& 等,如果推导出来的类型是 const 的,则声明 const 会被忽略掉,手动增加的左值引用或者右值引用符也会满足引用折叠。

int i = 1;
int &j = i;
const int k = 2;

int main() {
    decltype(i) & var1 = i; // var1 的类型为 int &
    decltype(j) & var2 = i; // var2 的类型为 int &,手动声明被忽略
    decltype((i)) && var3 = i; // var3 的类型为 int &,发生了引用折叠
    const decltype(k) &var4 = k; // var4 的类型为 const int &,const 被忽略,增加引用有效
    volatile decltype(k) var5 = k; // var5 的类型为 const volatile int
}

  对于 var5,const 和 volatile 是可以同时修饰一个变量的。const 修饰的变量是一个编译期的限制,编译期可能不会为 const 变量分配内存,意在限制本程序修改这个值;volatile 修饰的变量在编译期告诉寄存器不要做类似 const 的优化,一定要为变量分配内存,可用于其他程序可以修改本程序的值。而同时加上 const 和 volatile,则表示,请为这个常量分配内存,虽然本程序不会改变它,但是其它程序可能会改变它。

追踪返回类型

  追踪返回类型配合 auto 与 decltype 会真正释放 C++ 11 中泛型编程的能力。早期的 C++ 11 在使用模板的时候可能会出现尴尬的场景。如下所示,应该返回什么样的类型呢?只有返回值不同的函数不能参与重载,所以到最后考虑到完备性就会多些很多冗余的代码。

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

  在这里也不能用 decltype(t + u) 来替代 ???,因为编译函数时的顺序是从左到右。而函数使用前必须先声明。所以就需要 C++ 11 的两个关键字来配合使用,实现使用 auto 占位,使用 decltype 来推导类型,再将类型填入 auto 中。

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

  所以,auto 占位符和 ->return_type 也就构成了追踪返回类型函数的两个基本元素。

追踪返回类型可以用到的地方

替代普通函数

  对于普通函数来说,追踪返回类型会明显复杂一些。比如下面两个函数意义完全一样。

int func(char *a, int b);
auto func(char *a, int b) -> int;

  但是,对于多层命名空间嵌套的变量,还是有一定简化作用。因为追踪返回类型默认处于函数的命名空间或者类名之下的。

class OuterType {
    struct InnerType { int i; }
    InnerType GetInner();
    InnerType it;
}

auto OuterType::GetInner() -> InnerType {
    return it;
}

  返回值类型也就不需要写成 OuterType::InnerType 的形式。

简化函数定义

  C++ 有时会返回函数指针,这时候代码的可读性就会非常非常差。

int (*(*pf())())() {
    return nullptr;
}

  在面试题有时会有这样的考题。如果能够使用追踪返回类型简化这个考题,那么通过的概率也就大得多。

// auto (*)() -> int (*) () 外层是一个返回函数指针的 函数
// auto pf1() -> auto (*)() -> int (*)() 内层返回上面的函数指针
// 简化
auto pf1() -> auto(*)() -> int (*)() {
    return nullptr;
}

  追踪返回类型只需要依照从右往左的方式,就可以将嵌套的声明解析出来。大大地提高了可读性。

 
转载请带上本文永久固定链接:http://www.gleam.graphics/type-deduction.html

Tags :

About the Author

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注