Modern C++ 之 auto 与 decltype : 类型推导
变量类型推导其实在 C++ 中一直存在, 例如我们在使用泛型函数时编译器将帮助我们隐式地推导参数类型 :
1 | template<typename _Type> |
但直到 C++11 起才允许用户主动要求编译器进行类型推导. 现代 C++ 中提供的主动类型推导功能主要是通过 auto
的 decltype
两个关键字实现.
使用 auto
进行变量类型推导
当你声明一个变量为 auto
类型时, 编译器将自动帮助你推导出合适的数据类型. 这个变量可以是 :
- 声明后立即赋值的普通变量;
- 函数的返回值;
- 函数的形参 (C++14 起)
需要注意的是, 使用 auto
进行类型推导时, 将忽略顶层的 const
, &
, *
等修饰符, 以便用户更细化的控制推导, 示例见下 :
1 | int number = 8; |
这种基础的用法主要是用于省略一些很长的类型名, 一定程度上增加代码可读性. 一种经典用法是简写迭代器类型以遍历容器, 传统 C++ 中, 我们需要完整写出迭代器的类型或是使用局部的 typedef
进行简写, 如下 :
1 | std::unordered_map<std::string, std::string> key_value_map; |
而现代 C++ 中, 一方面我们通常使用 using
代替 typedef
, 但更方便的方式是使用 auto
简写迭代器类型 :
1 | // C++11 起, 使用 using 代替 typedef |
这里我们稍微引申一下, 在现代 C++ 中迭代一个容器的方式还有很多, 具体可见下面的例子 :
1 | // C++11 起, auto + 范围 for 循环 |
除了普通的变量外, 我们还可以使用 auto
设置函数的返回值, 此时需要我们在参数列表后使用 ->
符号标注具体的返回值类型. 这种写法被称作”返回类型后置语法”, 在一些脚本语言中比较常见 :
1 | auto add(int left, int right)->int { return left + right; } |
到了 C++14, 我们甚至可以使用 auto
进行参数类型推导, 在传统 C++ 中我们想进行参数类型的推导需要用到泛型机制, 但有了 auto
进行推导参数类型后, 我们可以简化一些工作 :
1 | // C++98/03 |
auto
关键字在单独使用时, 大部分是用于简化书写 当它与其它机制结合使用时可以衍生出更多的功能, 同样在下文里细说.
使用 decltype
进行表达式类型推导
auto
关键字用于推导变量类型, 与之相对的 decltype
则是用来推导表达式结果的类型. 熟悉 GCC 的用户可能对这个关键字不陌生, decltype
的标准化提案就是源自 GCC 的扩展关键字 __decltype
, 而后者又是源自于 GCC 一个很古老的扩展关键字 __typeof__
. 例如你可能需要推导某两个变量相加的结果类型 :
1 | auto number_a = 16LL; // long long number_a = 16LL; |
decltype
的推导规则遵循如下几点 :
- 若表达式是一个 不带括号的标记符表达式 或 类/结构体成员访问表达式, 那么推导的结果是所代表实体的类型;
- 若表达式是一个函数调用(包括操作符重载), 那么推导的结果是函数的返回类型, 若返回值是基础类型则抛弃
const
限定符; - 若表达式是一个字符串字面量, 则推到为
const
左值引用; - 上述情况以外, 若表达式结果为左值则推导为左值引用, 否则推导为本类型;
示例见下 :
1 | int number = 4; |
你可能在部分平台上使用过关键字 typeof
, __typeof__
或 __decltype
, 它们同样可用于推导表达式结果类型, 并且可以视作 decltype
功能的子集. 但这些关键字从来都不是标准 C++ 的一部分, 只是部分编译器支持的功能, 并且它们的推导规则也有很强的平台差异性. 相比之下 decltype
的标准化程度和适用面更广. 除此之外 typeof
进行类型推导时 &
引用符号很可能将不做保留, 至少 GCC 上是这样的, 参考以下示例 :
1 | int var = 1; |
总之, 当你的工程所使用的 C++ 版本若是等于或高于 C++11 , 我推荐全盘使用 decltype
代替 typeof
.
结合 auto
与 decltype
进行自动推导返回值类型
auto
与 decltype
单独使用的时候, 在功能上的突破本质还是向用户开放了主动要求类型推导的权限. 但如果二者结合使用的话, 就可以突破传统 C++ 中一些限制了. 在这里我们思考一个问题 : 如何实现一个满足所有类型之间进行 +
运算的函数?
在传统 C++ 中的最优解是这样的 :
1 | // C++98/03 |
这种做法的确可以满足所有类型的 +
运算, 但很明显使用上有着很大的局限性. 因为我们必须预知返回值的类型, 而题目的隐藏含义是一定要做到通用性的. 那么我们现在已经知道如何使用 decltype
可以进行表达式结果的类型推导, 那何不直接用其直接推导函数体的结果呢 ?
1 | template<typename _TypeLeft, typename _TypeRight, typename _TypeResult> |
很遗憾这种行为是无效的, 因为形式参的定义在参数列表里, 而处于参数列表左侧的返回值类型是无法获取形参名的. 除非我们能将返回值类型放在参数列表的右侧, 实现这个目标的方式就是使用 auto
书写返回类型后置语法 :
1 | // auto 返回类型后置语法 |
若只是单纯的只用 auto
进行返回类型后置, 则只是换了种语法. 但如果将 auto
与 decltype
相结合, 就可以突破传统 C++ 的限制了. 到此, 我们已经完全实现了题目里的需求. 但还有继续优化的空间, 首先是上文中提到的, 自 C++14 起, 我们可以利用 auto
进行参数类型的推导 :
1 | // C++14 |
其次是由于这种二者结合的模式被大量的使用, 自 C++14 起, 我们在使用 auto
描述返沪类型时无需在参数列表后写上返回类型, 编译器将自动通过函数体进行推导, 因此这个方法最终将演化成这种形式 :
1 | // C++14 |
引申 : 上例中的 “最终版本” 真的完美吗?
返回类型后置这种语法看似只是语法上的一些取巧手法, 但实则通过这种方式可以突破编译器的桎梏, 因为编译器始终是由上至下由左至右理解代码的.
正如上文中所说, auto
与 decltype
的功能并不是现代 C++ 才出现的, 而且在它们单独使用时更多的时候是一种简化代码书写的方式. 但当二者结合起来时, 将可以做出一些语言功能上的突破.
auto
与 decltype
的演化
传统 C++ 里, 类型推导一般是在模板传参时进行隐式类型推导, 而 auto
的出现是将类型推导的控制权开放给用户进行显示类型推导; 而传统 C++ 里表达式结果类型的推导通常由不同平台上各种 typeof
非标准扩展关键字实现, 而 decltype
的出现则是这个功能的标准化. 当 auto
与 decltype
相结合后, 由衍生出许多新的功能, 它们二者构成了现代 C++ 类型推导功能的核心. 以下的 auto
与 decltype
的发展历程简述 :
- C++11 :
- 允许使用
auto
进行普通变量的主动类型推导; - 允许使用
auto
书写返回值后置语法; - 使用标准
decltype
进行表达式结果类型推导以替代各平台的typeof
扩展关键字;
- 允许使用
- C++14 :
- 允许使用
auto
进行形参类型推导; - 允许使用
auto
进行返回值类型推导 (书写返回值后置语法时不适用类型标识符表面返回类型);
- 允许使用