通常我们认为 C++ 的模板主要是为了代码复用,即如果一组相似的类型 T 适用于一套算法,模板可以为类型 T 在编译期生成各自的算法代码,且不会有运行时开销。比如 C++ 的 STL 库,各类查找算法对不同容器都是通用的。
由于模板在编译期的各种特性,除了代码复用以外,模板也可有其他用途。例如,类模板可以被特化(specialization)和偏特化(partial specialization),并且编译器以 SFINAE(Substitution failure is not an error)的方式匹配模板,就可以做类型萃取(type trait);有了 constexpr
和非类型模板参数(non-type template parameter),就可以做复杂的编译期计算,提高运行时性能(但通常会让编译过程变慢,算是一种 trade off)。
所谓类型萃取,就是在编译期确定一个类型 T 是否具备某些特性(trait)。需要注意,“特性”并不是“类型”或者“接口”,不要用“类型”或“接口”去理解 trait,虽然它们很像,而且往往看起来一样。举一个“特性”的例子,比如我们想实现一个函数 func<T>(T t)
,但是我想要类型 T 必须有成员函数 StartTimer()
和 静态成员 变量 DefaultIntervalMs
,那么这里我们对类型 T 的要求,就是 trait。一个 trait 可以在一个新的维度,将一组类型划分为一类。
如果了解了特化和偏特化,以及 SFINAE,类型萃取其实非常简单,这些基础内容可以参看 C++ Template 进阶指南,我在参考链接里也会附上一些资料。这里假设我们已经了解了:
- 模板类的特化和偏特化,模板函数的特化,函数的重载
- 模板代码中的 dependent names 和
typename
关键字的用法 - SFINAE
- unevaluated operands/expression/context, 以及
decltype
和std::declval
的用法
对以上这些内容有一个感性认识,然后再多实践几次就能看懂很多类型萃取的代码了。大概搞懂以上内容后,可以继续往下看。
我把常见到的类型萃取技巧分为两种,一种使用类的特化和偏特化,另一种使用函数重载和 decltype
。这两种技巧本质上都是基于 SFINAE。
假设我们要通过 is_variant<T>
来判断类型 T 是否为 std::variant<Ts...>
,如何实现 is_variant<T>
呢?
方法一
以 is_variant<T>
为例,如果是使用类的特化和偏特化,我们可以这样写:
template <typename T>
struct is_variant_impl : std::false_type {
}; // 默认实现 (primary template)
template <typename... Ts>
struct is_variant_impl<std::variant<Ts...>> : std::true_type {
}; // 偏特化,T 对应 `std::variant<Ts...>`
template <typename T>
using is_variant = is_variant_impl<T>;
int main() {
std::cout << std::boolalpha << "is_variant<int> = " << is_variant<int>::v