技术: C++11 auto和decltype总结

主要讲讲编译时和运行时类型推导的用处和妙用, 一般是在模板编程中.

摘要: auto&decltype 表面上是为了实现C++11的新特性才引入的, 实际上编程中使用它会大大简化代码.

post-cover

最常见的情况恐怕是写vector<int>::iterator it, 这种名字过长的情景, 简化了代码的书写.

auto

编译时类型推导

(C++98/03中一直就有auto关键字了,只不过在C++98/03中auto关键字用于标识具有自动存储期的局部变量,它的作用并不大; 在C++11中auto关键字被重新利用了,用作类型推导)

写容器代码的时候, 尤其是迭代器, 算法之类的, 太长了&复杂了, 直接用auto吧, 编译器自动推导类型

比如:

1
2
auto x = 5; //auto即int
static auto y = 0.0; //auto即double

还有一种情况用auto, 即

需要给lambda表达式命名的时候, 匿名的仿函数, 很少能写出它的命名, 直接用auto吧.

《深入应用C++11 代码优化与工程机应用》专门研究过这个问题.

推导规则

理论上, 这里属于编译器实现的部分, 编译器会根据具体的情况推导出具体的类型.

  • 当表达式是一个引用类型的时候,auto会把引用类型抛弃,直接推导成原始类型
  • 当表达式是一个带cv限定符时,auto会把cv限定符也就是const抛弃,推导成non-const的
  • 当auto和引用类型相结合时, 编译器在推导时保留表达式的cv限定符

总结起来就:

  • 当不声明为指针和引用是,auto的推导结果类型将抛弃表达式的引用和CV限定符
  • 当声明为指针或引用时,auto推导结果将保留表达式的的cv属性
    1
    2
    3
    int x = 0;
    const auto &g = x;
    auto &h = g; //此时h的类型是 const int &, 即auto是保留了const属性

最好:

在传递const变量的时候,使用auto必须自己加const
auto不涉及引用和指针, 即&*, 在定义的时候, 单独写出来

注意事项

注意一下, auto的机制限制:

  • auto不能作为函数形参
  • auto不能修饰类或者结构体中的非静态成员变量
  • auto不能定义数组
  • auto不能推导出模板参数

它毕竟是编译时处理, 即编译时不能确定的, auto都做不了(运行时需要确定的请用decltype, 获取变量&对象&表达式的类型)

具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void func(auto a = 1){};  //error: auto不能用于函数形参,哪怕是带默认参数的形参  

struct MyStruct
{
auto var = 1; //error: auto不能修饰类或者结构体中的非静态成员
static auto var2 = 2;
};

template<class T>
struct Bar{};
int _tmain(int argc, _TCHAR* argv[])
{
int arr[10] = { 0 };
auto a = arr; //OK: a ->int *
auto r[10] = arr; //error : auto不能定义数组

Bar<int> bar;
Bar<auto> bb = bar; //error: auto 不能作为模板参数
return 0;
}

高级用法

一般高级用法是用在模板, 即元编程中, 用于运行时才能确定类型, 当前先用auto为了通过编译, 需要autodecltype配合.

例如, 某个方法, 由于某种原因, 其返回值当前不知道(取决于形参类型), 可以这么写:

1
2
template<typename X, tyname Y>
auto get(X x, Y y) -> decltype(x+y) {//具体实现}

如果你直接写decltype(x+y) get(X x, Y y)是不能通过编译的, 因为直到函数体执行时, 编译器才了解到 x 和 y.

decltype

decltype

有部分网友说这个关键词纯粹是为了开发标准库而引入的机制, 可能吧

C++11 也提供了从对象或表达式中“俘获”类型的机制, 相当于typeof(), 但是不同于 auto, 它永远能推导出实际调用对象的类型, 即运行时推导.

decltype永远推导出表达式的实际类型,不管你定义时是不是加了指针或者引用标记(一般不需要额外配合, 也不丢失属性)

推导规则如下:

  • expr是标识符,类访问表达式,decltype(expr)和expr的类型保持一致
  • expr是函数调用,decltype(expr)和返回值类型一致
  • expr是一个左值,则decltype(expr)是expr类型的左值引用,否则和expr类型一致

主要用途:

  • 用于返回值类型
  • 用于类型替代
  • 元编程(模板编程)

返回值

新的操作符 decltype 可以从一个表达式中“俘获”其结果的类型并“返回”.

再重复一下是上面 auto 的例子, 在《C++标准库2e》中有这么一个用法:

1
2
template<typename T1, typename T2>
decltype(x+y) add(T1 x, T2 y);

用来描述两个类型兼容(当然不是类型一致, 类型一致的话, 要一个模板参数即可)的两个变量的加操作.

可惜编译不通过, 因为decltype(x+y)时, 还不知道这两个变量, 只有在函数实现部分内部才知道.

所以编译器需要我们这样处理:

1
2
template<typename T1, typename T2>
auto add(T1 x, T2 y) -> decltype(x+y);

表示编译时, 编译器当做auto即可, 运行时根据decltype进行推到(或强转).

类型替代

其实它就是 gnu实现的typeof的标准版, 拿到类型, 然后用于下面的代码:

1
2
map<string, float> m;
decltype(m)::value_type elem;

再例如:

1
2
3
const vector<int> vi;  
typedef decltype (vi.begin()) CIT;
CIT another_const_iterator;

(不同于typeid, 它的功能非常有限, 只用于显示类型)

元编程

还有一种情况, 其实就是倒腾模板, 比如说, lambda表达式的的类型需要传递给模板, 例如下面的代码:

1
2
3
4
5
auto comp = [](const Person&p1, const Person&p2)->bool{
//具体的实现
};

std::set<Person, decltype(comp)> set1(comp);//如果这里不传入comp, 就

如果这里不传入comp, 即set1(comp);不指定用于比较的 functor , 那么编译器就会报错, 找不到lambda表达式类型的functor的构造器以及赋值函数.

其他例子:

1
2
3
4
5
6
template<typename T>
void test(T obj) {
typedef typename decltype(obj)::iterator iType;
//相当于
//typedef typename T::iterator iType;
}

其中typename 就是用来告诉编译器整个后面的decltype(obj)::iterator或者T::iterator确实是模板类型.

可能有人会觉得多此一举, 但是注意上面的, 模板第一编译的时候肯定通过, 而第二次编译, 即根据调用特化成模板函数时, 不一定成功,

因为T::iterator, 类型T必须要有iterator才行.


auto, decltype 对于元编程的内容的影响远不止如此, 水平有限, 先说这么多.

参考资料

  1. 《深入应用C++11 代码优化与工程机应用》
  2. 《C++标准库2e》
文章目录
  1. 1. auto
    1. 1.1. 编译时类型推导
    2. 1.2. 推导规则
    3. 1.3. 注意事项
    4. 1.4. 高级用法
  2. 2. decltype
    1. 2.1. 返回值
    2. 2.2. 类型替代
    3. 2.3. 元编程
  3. 3. 参考资料
|