C++ type_traits
一、什么是 type_traits
type_traits(类型萃取) 是一组模板类或函数,用于在编译时期获取或修改类型的信息。例如,你可以定义一个is_same 来检查两个类型是否相同,或者定义一个 remove_const 来去除类型的 const 限定符。
<type_traits> 头文件中包含了一组编译期工具,用于:
- 查询(Query):检查类型的属性(例如:是整数吗?是指针吗?是常量吗?)。
- 转换(Transform):根据现有类型生成新类型(例如:去除常量、添加引用、获取指针指向的类型)。
类型萃取可以帮我们检查和处理类型特性,从而优化代码、避免错误或提高性能。
C++11 引入了 <type_traits> 头文件,其中包含许多内置的类型萃取。下面是一些常见的例子:
std::is_integral:判断类型 T 是否为整数类型std::is_floating_point:判断类型 T 是否为浮点数类型std::is_pointer:判断类型 T 是否为指针类型std::is_reference:判断类型 T 是否为引用类型std::is_const:判断类型 T 是否为 const 类型std::is_same:判断类型 T 和 U 是否相同
这些类型萃取通常具有一个静态布尔值 value,当类型符合特定条件时,它为 true,否则为 false。
其实这些萃取函数原理都差不多,这里举一个例子说明下,比如 is_integral 用于判断一个变量是否是整形:
1 | |
在这个实现中,我们首先定义一个辅助模板 is_integral_helper,它默认继承自 std::false_type。
然后,我们为所有标准整数类型(包括有符号、无符号和字符类型)提供特殊化,使它们继承自 std::true_type。
核心思想就是为所有的整形提供一个特殊版本,其它非整形的就只能匹配到默认的版本,也就是 false_type
最后,我们定义 is_integral 作为 is_integral_helper 的一个包装,它首先移除给定类型的 const 和 volatile 限定符,然后应用 is_integral_helper。
std::true_type 和 std::false_type 是两个辅助类,分别用于表示编译时的 true 和 false 值。这两个类都有一个名为 value 的静态常量数据成员,它们的值分别是 true 和 false。
实际的实现可能更复杂,以适应各种编译器和平台的特性。
关键特性:
- 所有操作都在编译期完成,零运行时开销。
- 结果通常通过继承自
std::true_type或std::false_type来表示布尔值。 - 配合
if constexpr(C++17) 或std::enable_if(C++11/14) 使用,可实现基于类型的逻辑分支。
二、标准库中的类型萃取
举一个例子,如何使用类型萃取来选择不同的函数实现:
1 | |
三、分类
为了方便记忆,我们将常用的 trait 分为三类:检查类、修改类、辅助类。
3.1 检查类
返回 true 或 false,判断类型是否具有某种属性。C++ 14 后可以使用以 _v 结尾的类型,例如 std::is_integral_v<T>。
| 类别 | 常用 Trait | 说明 | 示例 |
|---|---|---|---|
| 基础属性 | std::is_integral |
是整数类型吗? | int, long -> true |
std::is_floating_point |
是浮点数吗? | float, double -> true |
|
std::is_array |
是数组吗? | int[] -> true |
|
std::is_pointer |
是指针吗? | int* -> true |
|
std::is_reference |
是引用吗? | int& -> true |
|
| 修饰符 | std::is_const |
有 const 修饰吗? | const int -> true |
std::is_volatile |
有 volatile 修饰吗? | volatile int -> true |
|
| 关系 | std::is_same |
T 和 U 是同一类型吗? | is_same -> true |
std::is_convertible |
T 能隐式转换为 U 吗? | is_convertible -> true |
|
| 构造特性 | std::is_trivial |
是平凡类型吗?(可 memcpy) | 普通结构体 -> true |
std::is_nothrow_move_constructible |
移动构造会抛异常吗? | 用于优化容器扩容 |
示例:
1 | |
3.2 修改类
根据输入类型 T,生成一个新的类型。这些通常以 _t 结尾(C++14 起),之前需要使用 ::type。
| Trait | 功能 | 示例 (T -> 结果) |
|---|---|---|
std::remove_const |
去除 const | const int -> int |
std::remove_reference |
去除引用 | int& -> int |
std::remove_cvref (C++20) |
去除 const, volatile, 引用 | const int& -> int |
std::add_pointer |
添加指针 | int -> int* |
std::add_lvalue_reference |
添加左值引用 | int -> int& |
std::decay |
非常重要:模拟函数参数传递后的类型衰减 (去引用、去 const、数组变指针) | const int[3] -> int* int() (函数) -> int(*)() |
示例:
1 | |
3.3 辅助类
std::enable_if: 如果B为 true,则定义成员type为T,否则无定义(触发 SFINAE)。std::conditional: 如果B为 true,type为T,否则为F(类似三元运算符)。std::void_t: 用于检测表达式的有效性 (C++17)。
四、应用场景
类型萃取之所以能“优化”代码,核心不在于缩短代码行数,而在于“消除运行时的冗余判断”和“实现编译期的多态选择”。
运行时做决策是低效的,利用类型萃取,让编译器在生成机器码之前,替你完成决策。
4.1 消除运行时的“虚函数”调用
假设我们要编写一个 Copy 函数,如果类型是简单的内置类型(如 int, double),我们直接用 memcpy;如果类型是复杂的对象,我们必须使用拷贝构造函数。
优化前(运行时判断):
1 | |
每次调用 CopyData 都要判断 is_pod,这会有分支预测的开销。
优化后(利用 if constexpr):
1 | |
对于 int 数组,编译器生成的二进制代码里根本不存在 else 分支。这相当于编译器为你“自动生成”了最优代码,运行时的 CPU 不需要做任何判断。
4.2 实现“编译期接口适配”
在 C++11/14 中,我们常用标签派发(Tag Dispatching)(见 C++标签派发(Tag Dispatching))来选择算法。
比如,处理“随机访问迭代器”(像 std::vector)和“链表迭代器”(像 std::list)。随机访问迭代器可以用 it + n 直接跳转,而链表必须 ++ n 次。
1 | |
这种技术在编译期就确定了调用哪个函数版本,完全避免了运行时的 if-else 分支跳转,实现了零成本抽象(Zero-cost Abstractions)。
4.3 类型安全与 API 简化
类型萃取还可以用来禁止某些错误操作,从而优化程序的健壮性。
假设你写了一个数学库,只希望用户传入数值类型:
1 | |
它将潜在的逻辑错误(比如试图对字符串做除法)从运行时的逻辑崩溃前置到了编译期的编译器报错。这减少了你调试的时间,也避免了生成产生异常代码的膨胀。
4.4 SFINAE 限制模板参数
SFINAE见 C++ SFINAE机制)
在 C++20 Concepts 出现之前,我们使用 std::enable_if 来启用/禁用函数。
1 | |