需要预先学习:


1. Traits(类型萃取)

写泛型代码时,你迟早会遇到一个问题:模版参数 T 到底是个什么类型? 是指针还是普通对象?是整数还是浮点数?去掉引用之后是什么?这些信息在编译期是存在的,但到了运行期已经"擦除"——if 只能根据值做判断,判断不了类型。

**Traits(类型萃取)**就是解决这个问题的技术:定义一类专门的 traits 类,把"关于类型的信息"编码成类成员——你给它一个类型,它告诉你一个编译期的事实(“是不是指针”),或给出一个关联的类型(“去掉指针后剩下的类型”)。“萃取”(extraction)指的正是从类型中提取这些隐藏信息的过程。

注:typeid 虽然能在运行期给出类型名,但泛型代码需要的是能参与编译期计算的答案——用来开分支、声明变量、约束模版,这些 typeid 都做不到。


1.1 用偏特化实现编译期分支

从一个需求出发:写一个泛型函数 reset——如果 T 是指针,就 delete 并置空;否则赋一个默认值。这个判断必须发生在编译期,而普通的 if 做不到:它的两个分支都要通过编译,delete x 对非指针类型直接报错。

答案藏在模版基础里讲过的偏特化(partial specialization)中。手写一个判断"T 是不是指针"的 traits:

#include <iostream>

// 主模版:默认情况——不是指针
template <typename T>
struct is_pointer
{
    static constexpr bool value = false;
};

// 偏特化:当 T 的形式是 U* 时匹配
template <typename T>
struct is_pointer<T*>
{
    static constexpr bool value = true;
};

int main()
{
    std::cout << std::boolalpha;
    std::cout << is_pointer<int>::value      << '\n';   // false
    std::cout << is_pointer<int*>::value     << '\n';   // true
    std::cout << is_pointer<double**>::value << '\n';   // true,匹配 T* 后 T = double*
}

编译并运行代码后,看到这样的输出:

false
true
true

工作过程:

  1. 使用 is_pointer<X> 时,编译器先检查偏特化能否匹配 X
  2. int 不符合 T* 的模式,落到主模版(primary template),value 为 false
  3. int* 符合 T* 的模式(此时 T = int),选择偏特化,value 为 true

本质上,模版偏特化就是编译期的 if-else:主模版是 else 分支,偏特化是 if 分支。标准库的 std::is_pointer 核心实现与此相同。traits 类通常没有任何数据成员和行为,它只是信息的载体,在运行期不占任何空间——一切都发生在类型系统里。


1.2 值 traits 与类型 traits

按回答的问题分,traits 有两种形态。

1. 值 traits:回答"是不是 / 是多少"

成员是一个编译期常量,比如 is_pointer 的 value。类似的还能表达"对齐是多少""是不是空类"等数值信息。

2. 类型 traits:回答"关联的类型是什么"

成员是一个类型别名,比如"去掉指针后剩下的类型":

#include <iostream>

// 主模版:没有指针可去,原样返回
template <typename T>
struct remove_pointer
{
    using type = T;
};

// 偏特化:T 的形式是 U* 时,交出 U
template <typename T>
struct remove_pointer<T*>
{
    using type = T;
};

int main()
{
    remove_pointer<int*>::type x = 10;   // x 的类型是 int

    std::cout << x << '\n';              // 输出 10
}

命名约定很固定:值 traits 的常量叫 value,类型 traits 的别名叫 type——整个标准库都遵守这个约定。


1.3 标准库的 type_traits

常用的类型问题,标准库已经封装在 <type_traits> 头文件中:

类别 代表 traits 回答的问题
类型属性 is_pointer、is_integral、is_class、is_empty T 具有什么性质
类型关系 is_same、is_base_of、is_convertible 两个类型是什么关系
类型变换 remove_reference、remove_pointer、remove_const、decay 由 T 得到关联类型
编译期选择 conditional、enable_if、common_type 按条件选出类型
#include <fstream>
#include <iostream>
#include <type_traits>

int main()
{
    std::cout << std::boolalpha;
    std::cout << std::is_integral_v<char>                        << '\n';   // true
    std::cout << std::is_floating_point_v<int>                   << '\n';   // false
    std::cout << std::is_base_of_v<std::istream, std::ifstream>  << '\n';   // true
}

标准库还提供了两个便捷后缀,替你省去 ::value 和 ::type:

原写法 后缀写法 引入版本
is_pointer<T>::value is_pointer_v<T> C++17
remove_pointer<T>::type remove_pointer_t<T> C++14

其中 decay 值得单独一提:它一次性完成"按值传参时发生的所有类型变化"——去引用、去顶层 const、数组退化为指针等。完美转发 里 std::forward 的实现,就用到了 remove_reference_t。


1.4 iterator_traits:萃取的经典应用

STL 算法是泛型代码,只操作迭代器、不关心容器。但算法经常需要知道迭代器指向的元素是什么类型——比如对区间求和,得用"元素类型"声明累加变量。

STL 的约定是:迭代器类内部自带 value_type 等别名。但裸指针(如 int*)也是合法迭代器,它没有内部别名可提供。std::iterator_traits 统一了这两个世界(简化实现):

// 主模版:真正的迭代器类——直接向它要
template <typename Iter>
struct iterator_traits
{
    using value_type = typename Iter::value_type;
};

// 偏特化:原生指针——元素类型就是 T
template <typename T>
struct iterator_traits<T*>
{
    using value_type = T;
};

typename 前缀是必需的:Iter::value_type 依赖模版参数 Iter,编译器看到它时不知道这是类型还是静态成员,typename 明确告诉它"这是个类型名"。

用它写一个对任意迭代器都有效的求和函数:

#include <iostream>
#include <iterator>
#include <list>
#include <vector>

template <typename Iter>
typename std::iterator_traits<Iter>::value_type sum(Iter first, Iter last)
{
    using T = typename std::iterator_traits<Iter>::value_type;

    T result{};
    for(Iter it = first; it != last; ++it)
        result += *it;
    return result;
}

int main()
{
    std::vector<int>  v{1, 2, 3, 4};
    std::list<double> l{1.5, 2.5};
    int               arr[]{1, 2, 3};

    std::cout << sum(v.begin(), v.end())             << '\n';   // 10
    std::cout << sum(l.begin(), l.end())             << '\n';   // 4
    std::cout << sum(std::begin(arr), std::end(arr)) << '\n';   // 6
}

vector<int>::iterator 和 list<double>::iterator 是正经的类,走主模版;arr 退化为 int*,走偏特化。同一个 sum 对三类迭代器都能工作,这就是 traits 作为"类型适配层"的价值。

注:iterator_traits 还提供 iterator_category(迭代器类别),std::advance、std::distance 等算法据此在编译期选择最优实现——随机访问迭代器直接 += n,前向迭代器只能循环 ++。思路与 is_pointer 完全一致。


1.5 enable_if:让 traits 约束模版

traits 的查询结果还能反过来约束模版:只有满足条件的类型才允许使用某个函数。C++20 concepts 出现之前,std::enable_if 是主要手段:

#include <iostream>
#include <type_traits>

// 只对指针类型开放:取出指向的值,空指针则返回默认值
template <typename T, typename = std::enable_if_t<std::is_pointer_v<T>>>
auto deref_or(T p, std::remove_pointer_t<T> fallback = {})
{
    return p ? *p : fallback;
}

int main()
{
    int  x = 10;
    int* p = &x;
    int* q = nullptr;

    std::cout << deref_or(p)     << '\n';   // 10
    std::cout << deref_or(q, 99) << '\n';   // 99
    std::cout << deref_or(x)     << '\n';   // 编译错误:T = int 不是指针
}

调用 deref_or(x) 时,编译器输出:

t6.cpp:13:26: error: no matching function for call to 'deref_or(int&)'
   13 |     std::cout << deref_or(x) << '\n';
      |                  ~~~~~~~~^~~
t6.cpp:5:6: note: candidate 1: 'template<class T, class> auto deref_or(T, std::remove_pointer_t<T>)'

原理:enable_if_t<条件, T> 在条件为 true 时就是 T;条件为 false 时根本不存在 ::type。模版实参替换失败不算硬错误(SFINAE,Substitution Failure Is Not An Error,替换失败并非错误)——这个重载被悄悄移出候选集,调用处得到一条直截了当的"没有匹配的函数"。

如果不加 enable_if,deref_or(x) 也能完成模版推导(T = int),错误会推迟到函数体内部的 *p 才爆发,报出一长串模版实例化信息。约束把拒绝提前到了替换阶段,错误信息更短、位置更准。


1.6 if constexpr:编译期分支

C++17 起,很多"按类型选行为"的场景可以用 if constexpr 配合 traits 完成,可读性比 enable_if 好得多。它正好解决开头的 reset 需求:

#include <iostream>
#include <type_traits>

template <typename T>
void reset(T& x)
{
    if constexpr(std::is_pointer_v<T>)
    {
        delete x;        // 指针:释放并置空
        x = nullptr;
    }
    else
    {
        x = T{};         // 普通类型:赋默认值
    }
}

int main()
{
    int* p = new int(10);
    int   v = 20;

    reset(p);
    reset(v);

    std::cout << (p == nullptr) << '\n';   // 1
    std::cout << v << '\n';                // 0
}

if constexpr 与普通 if 的区别在于:未被选中的分支不会被实例化。当 T = int 时,delete x 分支被编译器直接丢弃,即使它对 int 并不合法;当 T = int* 时,轮到 else 分支被丢弃。普通 if 的两个分支都必须通过编译——这正是开头无法直接写出 reset 的原因。

注:traits 负责把类型信息变成编译期常量,if constexpr 负责据此裁剪分支,两者配合覆盖了大多数"编译期分派"的需求。若要约束的是模版整体的可用类型,C++20 的 concepts 是更现代的写法。