详解C++17中的decltype类型推导
作者:@一鸣惊人
引子
在编程过程中,有时我们需要根据表达式的类型来声明变量,尤其是在涉及模板编程和泛型编程时,经常会遇到这样的问题:(1)、有些泛型类型由模板参数决定,但是却很难或根本无法表示;(2)、需要在编译时确定变量的类型。
除此之外,我们知道auto在自动类型推导时,会忽略类型的修饰符。如此会导致auto推导的类型会与原表达式的类型存在不一致问题。
为了更好的解决这些问题,从C++11标准开始,C++引入了decltype关键字,其作用是让编译器在编译时识别表达式的类型,方便的的进行类型推导,同时也解决泛型编程和模板编程中变量类型表示的问题。
标准演进
decltype
是declare type的缩写。C++11标准引入了decltype的核心功能和推导规则,C++11以后的各标准都本别对C++11自定的规则进行扩容和改进。具体演进过程如下所示:
- C++11:引入关键字,并引入decltype的核心功能,用于根据表达式推导出变量的类型;
- C++14:引入两个重要改进
- 引入decltype(auto)语法,此语法可用于函数返回值类型的推导。基于decltype(auto)语法,函数的返回值类型可通过函数体的返回值表达式来推导,从而简化函数返回值类型的声明。
- 放宽了对不完整类型的限制:在 C++11 中,如果 decltype 推导的表达式结果是一个不完整类型,那么会导致编译错误。而在 C++14 中,对不完整类型的处理更加宽松,允许使用decltype 推导不完整类型的变量。
- C++17:decltype(atuo)支持非类型模板形参占位符。
C++11
引入关键字,并引入decltype
的核心功能,用于根据表达式推导出变量的类型;当使用decltype(e)
推导表达式 e(类型为T)的类型时,C++11标准定义decltype
的推导规则如下:
- 如果是一个未加括号的标识符表达式或类成员访问,那么decltype(e)的推导结果为e类型T;假如不存在这样的实体或e是一组重载函数,那么decltype(e)无法推导。而且推导过程const/volatile 限定符会被忽略;
- 如果e是一个可调用对象,那么decltype(e)推导为可调用对象返回值的类型;
- 如果e是一个左值,decltype(e)推导为T&。const/volatile 限定符不能忽略;
- 如果e是一个将亡值,decltype(e)推导为T&&,const/volatile 限定符不可忽略;
- 如decltype(e)无法命中上述4情况,decltype(e)将会推导为e的类型T;
为了让大家更形象的理解这5条规则,下面我们通过一些示例来说明这五条推导规则。
示例 1: 未加括号标识符表达式
int x = 42; decltype(x) y; // 推导结果是 int,满足第1条规则
示例 2: 加括号的标识符表达式
int x = 42; decltype((x)) y = x; // 推导结果是 int&,满足第三条规则
示例3:未加括号的类成员访问
struct MyClass { int member; }; const MyClass obj; decltype(obj.member) result = obj.member; // 推导结果是 int, 忽略const/volatile 限定符,满足第1条规则
示例4:加括号的类成员访问
struct MyClass { int member; }; const MyClass obj; decltype((bj.member)) result = obj.member; // 推导结果是 const int&, const/volatile 限定符不能忽略,满足第3条规则
示例 5: 可调用对象表达式
int add(int a, int b) { return a + b; } decltype(add(1, 2)) result; // 推导结果是 int,满足第2条规则
示例 6: 将亡值
int x = 42; decltype(std::move(x)) result = std::move(x); // 推导结果为int&&,std::move(x) 为将亡值
示例 7: 右值表达式
int x = 42; decltype(x + 1) result; // 推导结果是 int(右值表达式 x + 1 的类型是 int)
示例8:右值引用变量
int&& i = 500; decltype(i) x2; // x2的类型是int&&,满足第5条
C++14
C++14主要引进了两个重要改进,他们分别是:放宽对不完整类型的限制;引入decltype(auto)
语法。
放宽对不完整类型的限制
C++11标准要求decltype
在使用时,推导的表达式必须是完整类型。如果decltype
推导的表达式是一个不完整类型,例如某个类的声明但尚未定义,那么会导致编译错误。C++14对这个限制进行了放宽,允许使用decltype
推导不完整类型的变量。这使得编写一些特定的模板代码更加方便,因为在某些情况下,可能需要推导出不完整类型。
但是,虽然C++14放宽了对不完整类型的限制,但仍然要求推导的表达式在使用时必须是可见的,即需要在推导时至少对类型进行了前向声明。否则,将会导致编译错误。
以下是一个示例,演示如何在泛型编程中使用 decltype 推导不完整类型:
template <typename T> struct Container { using ValueType = decltype(*std::declval<T>()); // 使用 decltype 推导不完整类型 // 其他成员和函数... }; int main() { Container<std::vector<int>> container; using ValueType = typename decltype(container)::ValueType; // 推导结果为 int& return 0; }
decltype(auto)
除了放宽对不完整类型的限制,C++14还有一个特色就是decltype(auto)
。decltype(auto)
作用是告诉编译器auto的推导规则遵循decltype而非auto。不过有一点需要注意就是decltype(auto)必须单独声明,不能与其他相结合。所以下述声明是不合法的:decltype(auto)*
,const decltype(auto)
, volatile decltype(auto)
。
decltype(auto)
的推导规则如下:
- 如果初始化表达式是一个标识符表达式,那么decltype(auto)推导为表达式的类型(const/volatile 限定符和引用修饰符不能忽略);
- 如果初始化表达式是一个函数调用表达式,那么decltype(auto)推导为函数调用表达式的返回类型;
- 如果初始化表达式是一个左值表达式(如变量名、数组名、成员访问等),那么decltype(auto)推导为对应左值类型的引用类型(const/volatile 限定符和引用修饰符不能忽略)。
- 如果初始化表达式是一个右值表达式(如字面值、临时对象、表达式的结果等),那么decltype(auto)推导为对应右值的类型(const/volatile 限定符和引用修饰符不能忽略)。
- 如果初始化表达式是一个将亡值(如移动赋值),那么decltype(auto)推导为对应类型的右值引用
示例 1:标识符表达式
int x = 42; decltype(auto) y = x; // 推导结果是 int(x 的类型)
示例 2:函数调用表达式
int add(int a, int b) { return a + b; } decltype(auto) result = add(1, 2); // 推导结果是 int(add 函数返回类型)
示例 3:左值表达式
const int x = 42; decltype(auto) ref = (x); // 推导结果是 const int&(x 的引用类型)
示例4:右值表达式
decltype(auto) x2 = 50; // 推导结果是 int
示例4:将亡值
int x2 = 50; decltype(auto) x3 = std::move(x2); // 推导结果为int&&
除了变量类型推导以外,在C++14中引入了decltype(auto)作为一种返回类型的语法。它用于在函数声明中指定返回类型,该返回类型将从函数体中的表达式推导而来。
为了更好的理解decltype(auto)作为一种返回类型的语法,我们参考下面三种函数返回类型自动推导定义方式。
第一种: C++14 基于auto新特性返回值类型自动推导
template<typename Container, typename Index> auto accessOrUpdate(Container& c, Index i) { return c[i]; // 返回类推导为c[i]的类型,而且会异常引用限制 } std::vector<int> v{1,2,3,4,5}; accessOrUpdate(v,2) = 10; // 编译错误,不允许赋值
第二种:C++14 基于auto和decltype实现返回值类型推导
template <typename Container, typename Index> auto accessOrUpdate(Container &c, Index i) -> decltype(c[i]) { return c[i]; } std::vector<int> v{1,2,3,4,5}; accessOrUpdate(v,2) = 10;
第三种:C++14 decltype(auto)实现返回值类型推导
template <typename Container, typename Index> decltype(auto) accessOrUpdate(Container &c, Index i) { return c[i]; } std::vector<int> v{1,2,3,4,5}; accessOrUpdate(v,2) = 10;
对比上述三种函数返回值类型推导,decltype(auto)可让编译器根据表达式的类型自动推导函数的返回类型,而不需要显式地指定返回类型。这种方式可简化代码,而且推导更加灵活。
C++17
为与auto交相辉映,C++17开始支持decltype(auto)非类型模板。但是需特别注意的是在C++17标准中,非类型模板参数类型必须是整理类型(int, short, long等),枚举类型,指针类型,左值引用类型和std::nullptr_t,而自定义类型,浮点数和字符串则不允许作非类型模板参数。
template<decltype(auto) n> // C++17 decltype(auto)形参声明 auto f() -> std::pair<decltype(n), decltype(n)> // auto 不能从花括号初始化器列表推导 { return {n, n}; } f<5>(); // n为int f<(5)>(); // n为int& f<'a'>(); // n为char f<('a')>(); // n为char& f<1.0>(); // 编译失败double不能作为模板参数,double不允许做非类型模板参数。
C++20允许字面量类类型作为非类型模板参数。例如在C++20之前,下述代码无法编译通过,而在C++20中则可以编译通过。
class A {}; template <A a> class B {}; A a; B<a> b; // C++20 前编译失败,C++20 可以编译成功。
总结
本文从泛型编程中经常会遇到2个常见问题入手,循序渐进的分析了从C++11开始引入的关键字decltype,希望本文可以对大家有所帮助。