C++ 中的类型萃取(Type Traits)是一种在编译时查询和操作类型信息的模板元编程(Template Metaprogramming)技术。它允许你检查一个类型的各种属性,例如它是否是整数、指针、引用、常量等等,或者对类型进行转换,例如添加或移除const限定符

这项技术的核心是<type_traits>头文件,它提供了一系列模板类,这些类在编译期提供关于类型的信息。


核心思想与工作原理

类型萃取的基本工作方式是:

  1. 定义一个模板结构体(例如std::is_integral<T>)。
  2. 通过模板特化,为不同类型提供该结构体的特化版本。
  3. 这些结构体中通常包含一个静态成员变量value(对于查询类萃取)或一个类型别名 type(对于转换类萃取)。 当我们使用一个具体的类型(如 int)来实例化这个模板时,编译器会根据特化规则选择最匹配的版本,并返回其内部的 valuetype

例子:去除一个类型的 constvolatile 限定符(转换类萃取)

要去除一个类型Tconstvolatile限定符可以调用std::remove_cv<T>::type或者直接调用更简洁的std::remove_cv_t<T>(二者是等价的)。

下面我们看看这是如何实现的。在<type_traits>头文件中有这样一段代码:

/// remove_cv
template<typename _Tp>
struct remove_cv
{ using type = _Tp; };
 
// 特化版本
template<typename _Tp>
struct remove_cv<const _Tp> 
{ using type = _Tp; };
 
template<typename _Tp>
struct remove_cv<volatile _Tp>
{ using type = _Tp; };
 
template<typename _Tp>
struct remove_cv<const volatile _Tp>
{ using type = _Tp; };

可以看到,实际实现非常的简单,仅需对包含constvolatile限定符的类型实现特化版本的结构体,在编译时编译器就会自动找到最匹配的特化版本,从而实现去除限定符。

remove_cv_t的实现如下:

template<typename _Tp>
using remove_cv_t = typename remove_cv<_Tp>::type;

去除引用也是同理:

/// remove_reference
template<typename _Tp>
struct remove_reference
{ typedef _Tp   type; };
 
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp   type; };
 
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp   type; };

我们也可以参考 Nlohmann Json 库,自己实现一个同时去除constvolatile限定符和引用的类型萃取:

template<typename T>
using uncvref_t = typename std::remove_cv<typename std::remove_reference<T>::type>::type;

转换类萃取的实现类似,此处不再赘述。

根据类型选择不同实现

类型萃取可以与if constexpr(C++17)结合使用,在编译时为不同类型的模板选择不同的代码实现。一个简单的例子:

#include <iostream>
#include <type_traits>
 
template <typename T>
void print_details(T value) {
    if constexpr (std::is_pointer<T>::value) {
        std::cout << "It's a pointer pointing to: " << *value << std::endl;
    } else if constexpr (std::is_integral<T>::value) {
        std::cout << "It's an integral with value: " << value << std::endl;
    } else {
        std::cout << "It's some other type." << std::endl;
    }
}
 
int main() {
    int x = 10;
    int* ptr = &x;
 
    print_details(x);   // 输出: It's an integral with value: 10
    print_details(ptr); // 输出: It's a pointer pointing to: 10
    print_details(3.14); // 输出: It's some other type.
 
    return 0;
}

enable_if

<type_traits>头文件中还有一个常用的类型萃取: std::enable_if<Cond, T>,定义如下:

template<bool, typename _Tp = void>
struct enable_if
{ };
 
// Partial specialization for true.
template<typename _Tp>
struct enable_if<true, _Tp>
{ typedef _Tp type; };

注意到,非特化版本的结构体中不存在type成员变量。为什么要这样设计呢?这与SFINAE原则有关,与它结合,我们可以实现选择性地实例化模板函数。首先我们先来了解一下SFINAE是什么。

SFINAE

SFINAE (Substitution Failure Is Not an Error, 替换失败并非错误)是一种在模板元编程中常见的技术,其核心思想是:当编译器在模板特化过程中尝试进行类型替换,如果替换导致了无效的代码(例如,访问一个不存在的成员),它不会直接报错,而是会放弃这个特化版本,去寻找其他可用的版本。

现在我们可以理解为什么enable_if要这样设计了。假设我们想只对整数类型和浮点数类型实现某个函数:

templete<typename T,
         std::enable_if_t<
            std::is_integer_v<T> || std::is_floating_point_v<T>,
         int> = 0>
void func() {
    std::cout << "Integer or floating point!" << std::endl;
}

此时,对于其他类型,编译器在尝试实例化func函数时,会发现std::enable_if_t调用了不存在的成员函数,导致实例化失败!这种实现方式在我们需要根据相对复杂的分类标准来对不同类型实现不同代码实现时非常有用。

void_t

std::void_t也是一个常用的类型萃取,它的定义非常简单:

template<typename...> using void_t = void;

它的作用是检测模板参数中的类型是否有效,若全部有效,则返回void类型,否则替换失败。当我们想检测一个类是否具有某个成员函数时,它会非常有用。

更复杂的例子:is_compatible_type

我们回到 Nlohmann Json 库中。该库利用了许多复杂的类型萃取来实现多样的类型性质的检测。我们以其中一个应用为例来分析一下。

basic_json有一个构造函数是这样声明的:

template < typename CompatibleType,
            typename U = detail::uncvref_t<CompatibleType>,
            detail::enable_if_t <
                !detail::is_basic_json<U>::value && detail::is_compatible_type<basic_json_t, U>::value, int > = 0 >
basic_json(CompatibleType && val) noexcept(noexcept(
            JSONSerializer<U>::to_json(std::declval<basic_json_t&>(),
                                        std::forward<CompatibleType>(val))))

其中运用到了一个类型萃取is_compatible_type。它的作用是判断一个类型是否兼容,也就是是否实现了对应类型的序列化器特化版本或者重载了对应类型的to_json函数(通过前一篇文章的分析,我们知道只需要判断JSONSerializer<U>是否存在to_json函数即可)。

template<typename BasicJsonType, typename CompatibleType, typename = void>
struct is_compatible_type_impl: std::false_type {};
 
template<typename BasicJsonType, typename CompatibleType>
struct is_compatible_type_impl <
    BasicJsonType, CompatibleType,
    enable_if_t<is_complete_type<CompatibleType>::value >>
{
    static constexpr bool value =
        has_to_json<BasicJsonType, CompatibleType>::value;
};
 
template<typename BasicJsonType, typename CompatibleType>
struct is_compatible_type
    : is_compatible_type_impl<BasicJsonType, CompatibleType> {};

首先我们看到,is_compatible_type继承自一个基类is_compatible_type_impl,这是为了引入最后一个参数typename = void从而实现SFINAE,同时提供一个干净的接口。is_compatible_type_impl继承自std::false_type(这意味着所有类型默认为不可兼容的)。对于完整的类型,is_complete_type<CompatibleType>::valuetrue(其实就是检测能否用sizeof()获得类型的大小,此处省略相应代码),此时我们将value设置为has_to_json<BasicJsonType, CompatibleType>::value

template<typename T, typename... Args>
using to_json_function = decltype(T::to_json(std::declval<Args>()...));
 
template<typename BasicJsonType, typename T, typename = void>
struct has_to_json : std::false_type {};
 
template<typename BasicJsonType, typename T>
struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json<T>::value >>
{
    using serializer = typename BasicJsonType::template json_serializer<T, void>;
 
    static constexpr bool value =
        is_detected_exact<void, to_json_function, serializer, BasicJsonType&,
        T>::value;
};

这里又引入了一个类型萃取,is_detected_exact

template<class Default,
         class AlwaysVoid,
         template<class...> class Op,
         class... Args>
struct detector
{
    using value_t = std::false_type;
    using type = Default;
};
 
template<class Default, template<class...> class Op, class... Args>
struct detector<Default, void_t<Op<Args...>>, Op, Args...>
{
    using value_t = std::true_type;
    using type = Op<Args...>;
};
 
template<template<class...> class Op, class... Args>
using detected_t = typename detector<nonesuch, void, Op, Args...>::type; // nonesuch 是一个自定义的不含任何成员变量的类型,作为检测失败时的默认返回类型
 
template<class Expected, template<class...> class Op, class... Args>
using is_detected_exact = std::is_same<Expected, detected_t<Op, Args...>>;

从这里我们可以看出来,is_detected_exact的作用是检测一个函数是否存在,且返回值是否和预期相同。第一个参数是预期函数返回值的类型,第二个参数用来传递函数(这里就是to_json_function,它接受两个模板参数(不是函数参数!),分别指定序列化器类型函数参数类型),剩余模板参数是需要传递进函数的参数类型。这个检测方式非常巧妙!它的拓展性非常强,我们只需要做很小的改变就可以实现检测是否存在函数参数类型和返回值类型都和预期一致的成员函数!

总结

C++的类型萃取是一种十分有效的技术,它非常好地体现了C++的设计哲学:

  • 编译时即一切:错误应该尽可能在编译时被发现,而不是运行时
  • 性能至上:抽象不应以牺牲性能为代价,通过编译时技术实现零开销抽象
  • 追求极致的泛化能力:为泛型编程提供精细的、类型驱动的控制能力,使其既灵活又高效。 通过类型萃取,泛型编程的安全性得到了更大的保障,我们得以实现更智能地生成模板代码,实现算法的最优化。