Wang's blog

类型推导

Published on

C++是强类型语言,使用每个变量前必须声明其类型。如果不进行类型推导,则用户必须自行声明所有变量的类型,对于一些复杂的类型(如容器的迭代器)或临时类型(如lambda表达式),这是没有意义甚至无法完成的。为此,C++11标准引入了auto与decltype两个关键字用于进行类型推导,使得编译器可以自动推导出变量的类型。这样做有以下好处:

  • 鲁棒性:即使更改表达式的类型也能正常工作
  • 性能:可以保证没有进行转换
  • 可用性:不必担心类型名称拼写困难或拼写有误
  • 效率:代码会变得更高效

auto

在C++11之前,auto关键字为存储期说明符,但是基本不使用。C++11之后将此关键字用于类型推导,其基本语法是:

auto 变量名 初始化器;

基本用法

// 通用初始化语法
auto a{42};
// 赋值语法
auto b = 0;
// 通用赋值语法,它结合了上述两种形式
auto c = {3.14159};
// 直接初始化或构造函数样式的语法
auto d(1.41421f);

// 通过表达式推导
auto e = 1 + 2;
// 通过函数返回值推导
auto f = add(1, 1.2);
// 推导new操作符结果
auto g = new auto(10);
// 推导lambda表达式类型
auto lambda1 = [](int x) { return x + 3; };

// 可在一行定义多个变量,但是不能出现冲突
auto x = a, &y = b, *z = &e;

// C++14后可自动推导函数返回值
auto func() { return 2 + 3; }
// C++14后可自动推导lambda函数的参数
auto lambda2 = [](auto a) { return a; };
// C++17后可自动推导非类型模板参数
template <auto I> struct A;

// 在for循环中使用
auto vec = std::vector<int>();
for (auto it = vec.cbegin(); it != vec.cend(); ++it) {}
for (auto v : vec) {}

使用大括号初始化时的类型推导

auto A = {1, 2};   // std::initializer_list<int>
auto B = {3};      // std::initializer_list<int>
auto C{4};         // C++17后为int,之前为std::initializer_list<int>
auto D = {5, 6.7}; // 错误,初始化列表中元素类型不同
auto E{8, 9};      // C++17后非法,之前为std::initializer_list<int>

引用与cv限定符

使用auto进行类型推导时会删除引用、const限定符和volatile限定符,如有需要可自行添加。对于指针无影响。

// 一个int类型与一个int&类型变量
int a = 10;
int &b = a;

// 使用auto会删除引用
auto c = a;         // c为int
auto d = b;         // d同样为int

// 手动添加引用、const或volatile
auto &e = a;        // e为int&
const auto f = b;   // f为const int

// 对于指针,auto与auto*均可
auto g = &a;        // g为int*
auto *h = &a;       // h为int*

错误使用

// 必须初始化
auto a;

// 一行内出现冲突
auto b = 1, c = 1.2;

int arr[10] = {0};
// auto_arr推导为指针
auto auto_arr = arr;
// 不能用于推导数组
auto auto_arr2[10] = arr;

decltype

decltype关键字是为了解决auto关键字只能对变量进行类型推导的缺陷而出现的,其基本语法如下:

decltype(实体/表达式)

编译器推导参数的类型并返回,不会实际计算表达式的值。

推导规则

  • 如果参数是没有括号的标识表达式或没有括号的类成员访问表达式,返回该实体的类型
  • 如果参数是其它类型为T的表达式
    • 如果表达式是将亡值,返回T&&
    • 如果表达式是左值,返回T&
    • 如果表达式是纯右值,返回T
  • 如果对象带有括号,那么它会被当做左值表达式,从而decltype(x)和decltype((x))通常是不同的类型。
  • decltype与auto不同,decltype不会删除引用、const限定符与volatile限定符。
struct A { double x; };
const A *a;

decltype(a->x) y;           // y的类型是double(其声明类型)
decltype((a->x)) z = y;     // z的类型是const double&(左值表达式)

int i = 33;
decltype(i) j = i * 2;      // j的类型为int

// 一个lambda表达式,其类型是独有且无名的
auto f = [](int a, int b) -> int
{
    return a * b;
};

decltype(f) g = f;          // g的类型与f相同

自动推导函数的返回值类型

如果一个函数的返回值类型可由其参数类型确定,如下面的函数:

template <typename T, typename U>
decltype(x + y) add(T x, U y)
{
    return x + y;
}

但是上述写法是错误的,在C++11中可以使用拖尾返回类型解决:

template <typename T, typename U>
auto add(T x, U y) -> decltype(x + y)
{
    return x + y;
}

从C++14开始,可以直接使用auto进行推导:

template <typename T, typename U>
auto add(T x, U y)
{
    return x + y;
}

decltype(auto)

使用auto进行类型推导会删除引用与cv限定符,如果想要保留,可以使用decltype(auto)。

int a = 0;
// 使用decltype(auto) x = expr;推导变量类型,相当于decltype(expr) x = expr;
decltype(auto) c1 = a;      // c1的类型是int
decltype(auto) c2 = (a);    // c2的类型是int&

decltype(auto) z = {1, 2};  // 错误,{1, 2}不是表达式

// 使用decltype(auto)推导函数返回值类型,可保留返回值的引用与cv限定符
template <class F, class... Args>
decltype(auto) PerfectForward(F fun, Args &&...args)
{
    return fun(std::forward<Args>(args)...);
}