浅谈C++类型推导

C++ 11引入了auto/decltype关键字来进行类型推导,其在一些场景中可以提高代码简洁性和可读性。 auto关键字的类型推导规则基本上和模板参数类型推导规则一致,Effective Modern C++ Chapter 1对此有很详细的讨论,本文的主要参考内容也是该书。 decltype可以原汁原味地返回变量名对应的变量类型,其可以完成auto关键字不能胜任的一些corner case任务。 本文接下来将以较短的篇幅对这三者进行介绍,希望能对大家有所帮助。

1. 模板参数类型推导

对于函数模板 ​template<typename T> f(T&|T*|T&&|T)​,其模板参数类型的可选类型包括T&|T*|T&&|T. 若f(ParamType)为f(T|…)完成类型推导后的结果,那么ParamType则为类型推导完成后的实际函数入参类型。 当函数实参为相关int类型时,类型推导结果如下:

类型 指导思想 ParamType T 是否保留const/volatile属性
T 传值/复制 int int
T&, T* 传引用/指针 int&, int* int
T&& 完美转发 int& (lvalue), int&& (rvalue) int& (lvalue), int (rvalue)

其中,T的类型推导指导思想为确保f()的传参方式为传值,T&/T*则为传引用/指针,而T&&则是为了确保f()可以实现完美转发。 当模板参数类型为T时,const/volatile属性都不会被保留,这是传值复制规则规定的。 例如,对于f(T), 若入参为const char * const ptr, 则ParamType和T推导结果都是const char *, 因为对const ptr进行复制时不会保留其const属性。

当实参为数组时,f(T)传值复制规则下实参将被退化为指针,而f(T&|T&&)则不会,其可以保留数组维度信息。 例如,当函数实参为​int a[N]​时,类型推导结果如下:

类型 ParamType, T 备注
T int* 退化为指针
T&, T&& int(&)[N] 保留了数组维度信息

函数实参也可能会被退化为函数指针,其在类型推导时会遵循和数组实参类似的规则。 例如,当函数实参为​void f(int)​时,类型推导结果如下:

类型 ParamType, T 备注
T void (*)(int) 退化为函数指针
T&, T&& void (&)(int)  

2. auto

auto类型推导规则和template参数类型推导基本一致,它们之间的映射的关系如下:

auto T 传值/复制
auto& T& 传引用
auto&& T&& 完美转发

auto和template参数类型推导规则唯一不同之处在于当实参为初始化列表时(如​auto il = {1, 2, 3}​),此时auto的推导结果为std::initializerlist<T>, 而函数模板参数推导则会失败。 此外,当auto作为函数返回值或lambda入参类型时,auto类型推导规则将和template参数类型推导规则完全一致。

3. decltype

decltype可以原汁原味地不加任何修饰地返回变量名的类型, 其类型推导规则如下:

入参类型 推导结果
变量名 变量名对应的变量类型
lvalue表达式 T&
xvalue表达式 T&&
prvalue表达式 T

此外,C++ 14还引入了decltype(auto), 其主要被用于推导函数返回值类型。 decltype(auto)采用的推导规则为decltype规则,而不是auto推导规则。 decltype(auto)可以完成一些auto不能完成的任务,比如下面代码(源自Effective Modern C++ Item 3: Understand decltype)中的模板函数出参类型推导:

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i) {
    authenticateUser();
    return c[i];
}

若仅使用auto, 那么出参类型推导规则为传值复制,最终推导结果将不是lvalue引用而不能满足需求。 而decltype(auto)则采用decltype类型推导规则,其对lvalue表达式的推导结果为lvalue引用。