类型萃取

1. 类型萃取

1.1 核心概念

类型萃取这是 C++11 引入的特性(并在 C++14/17/20 中得到了极大的扩展),是进行 模板元编程 和编写通用泛型代码的基础工具。它提供了一系列编译时的模板类和模板别名,用于在编译期间检查、修改和操作类型。这些功能主要集中 C++ 标准库的 <type_traits> 头文件中。

在 C++ 中文技术社区和文献中,<type_traits> 通常有以下几种常见的称呼,根据上下文略有不同:

  1. 官方翻译:类型萃取,意为从类型中提取信息。

    在 C++ 标准库中文版文档以及许多经典书籍(如《C++ Primer》译本)中,通常使用这个词来描述这种在编译期获取类型特性的机制。

  2. 编程术语:类型特性

    <type_traits> 头文件提供的正是各种“类型”的“特性”(比如是否有 const 修饰符、是否是整数、是否是类等)。

  3. 通俗称呼:类型工具类型辅助

    在日常开发交流中,程序员很少专门说出一个学术名词,通常会说用一下 type_traits或用类型工具判断一下。

类型萃取允许在编译期确定类型的某种属性。例如,判断一个类型是否是指针、是否是 const 修饰的、两个类型是否相同,或者获取类型的成员变量类型等。

在编写模板代码时,我们经常需要对不同的类型进行不同的处理。例如,一个排序函数模板,针对内置类型(如 int)可以使用快速拷贝(如 memcpy),而针对自定义类类型(如 Student)则必须调用拷贝构造函数。类型萃取帮助编译器做出这种选择。

大多数 <type_traits> 中的类都有一个共同的结构模式。它们通常是一个类模板,接受一个或多个类型参数,并包含以下成员:

  • value: 一个 static constexpr 成员变量。
    • 如果是 判断类 traits,它是 bool 类型(如 truefalse)。
    • 如果是 变换类 traits,它可能是整数或其他值(如 std::integral_constant)。
  • type: 一个公有的 typedefusing 定义的类型别名。
    • 如果是 判断类 traits,它通常是输入类型本身(主要用于继承)。
    • 如果是 变换类 traits,它是变换后的新类型。

C++17<type_traits> 做了一些增强,使得代码更加简洁易读。C++11/14 时期,我们写代码比较啰嗦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <type_traits>
#include <iostream>

int main()
{
// C++11/C++14 风格, 判断 int 是否为整型
std::cout << std::is_integral<int>::value << std::endl; // 输出 1 (true)

using NoConstT1 = std::remove_const<const int>::type;
// 检查一下是否正确
std::cout << std::is_same<NoConstT1, int>::value << std::endl; // 输出 1 (true)

return 0;
}

C++17 引入了 _v_t 后缀(其实 _t 是 C++14 引入的,但在 17 中已成为标配习惯):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <type_traits>
#include <iostream>

int main()
{
// C++17 风格 (更加简洁)
std::cout << std::is_integral_v<int> << std::endl; // 输出 1 (true)

// 必须指定类型:std::remove_const_t<const int>
using NoConstT1 = std::remove_const_t<const int>;
// 检查一下是否正确
std::cout << std::is_same<NoConstT1, int>::value << std::endl; // 输出 1 (true)

return 0;
}

2. 详细特性分类表 (C++17 版)

以下列出的所有 xxxx_v 变量模板均定义为 inline constexpr bool xxxx_v = xxxx::value;

下面表格并未列出所有类,如有需要可自行查询类型萃取相关类 API 在线文档

2.1 基础类型判断

用于检查类型 T 是否具有某种基础性质。

特性类 C++17 变量模板 描述
is_void<T> is_void_v<T> 是否是 void
is_null_pointer<T> is_null_pointer_v<T> 是否是 std::nullptr_t
is_integral<T> is_integral_v<T> 是否为整型 (int, char, bool 等)
is_floating_point<T> is_floating_point_v<T> 是否为浮点型 (float, double)
is_array<T> is_array_v<T> 是否为数组
is_pointer<T> is_pointer_v<T> 是否为指针
is_lvalue_reference<T> is_lvalue_reference_v<T> 是否为左值引用
is_enum<T> is_enum_v<T> 是否为枚举类型
is_union<T> is_union_v<T> 是否为联合体
is_class<T> is_class_v<T> 是否为类或结构体
is_function<T> is_function_v<T> 是否为函数类型
is_member_pointer<T> is_member_pointer_v<T> 是否为成员函数指针或成员变量指针
is_arithmetic<T> is_arithmetic_v<T> 是否为算术类型: 整数或浮点数
is_fundamental<T> is_fundamental_v<T> 是否为基本类型: void、整数、浮点、nullptr
is_object<T> is_object_v<T> 是否为对象类型
is_compound<T> is_compound_v<T> 是否为复合类型
is_reference<T> is_reference_v<T> 是否为引用:左值或右值引用

2.2 类型属性判断

检查类型的更深层属性。

特性类 C++17 变量模板 描述
is_const<T> is_const_v<T> 是否有 const 修饰
is_volatile<T> is_volatile_v<T> 是否有 volatile 修饰
is_trivial<T> is_trivial_v<T> 是否为平凡类型 (可安全 memcpy)
is_standard_layout<T> is_standard_layout_v<T> 是否为标准布局 (兼容 C 语言)
is_pod<T> (C++20起移除) is_pod_v<T> 是否为 POD 类型 (既是 trivial 又是 standard_layout)
is_empty<T> is_empty_v<T> 是否为空类 (无数据成员,虚函数通常不为空)
is_polymorphic<T> is_polymorphic_v<T> 是否为多态类 (含虚函数)
is_abstract<T> is_abstract_v<T> 是否为抽象类 (含纯虚函数)

2.3 类型关系判断

检查类型之间的相互关系。

特性类 C++17 变量模板 描述
is_same<T, U> is_same_v<T, U> TU 是否完全相同
is_base_of<Base, Der> is_base_of_v<Base, Der> Base 是否是 Der 的基类
is_convertible<T, U> is_convertible_v<T, U> T 是否可隐式转换为 U
is_invocable<...> is_invocable_v<...> [C++17] 给定参数是否可调用
invoke_result<F, Args> invoke_result_t<F, Args> 推导调用结果的类型

在 C++17 之前,很难非常精准地判断一个对象是否可以被调用,以及调用的结果类型是什么。C++17 引入了一系列工具:

  • is_invocable<Callable, Args...>: 判断 Callable 是否可以用参数 Args... 调用。
  • is_invocable_r<Result, Callable, Args...>: 判断是否可调用,且返回类型可转换为 Result
  • is_invocable_r_v<R, Callable, Args...> 不仅检查能不能调用,还要检查调用的返回值是否可以隐式转换R
  • invoke_result_t<Callable, Args...>:(C++17) 推导调用结果的类型(替代已废弃的 result_of( C++11 ))。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <type_traits>
#include <string>

void func(int a)
{
std::cout << "Function called with " << a << std::endl;
}

struct Functor
{
void operator()(double d) {}
};

int main()
{
// ==========================================
// 1. is_invocable 测试
// ==========================================
std::cout << "--- is_invocable checks ---" << std::endl;

// 检查 func 是否能被 int 参数调用
std::cout << "func(int): " << std::is_invocable_v<decltype(func), int> << std::endl; // true (1)

// 检查 func 是否能被 double 参数调用 (隐式转换)
std::cout << "func(double): " << std::is_invocable_v<decltype(func), double> << std::endl; // true (1)

// 检查 func 是否能被 string 参数调用 (不允许)
std::cout << "func(string): " << std::is_invocable_v<decltype(func), std::string> << std::endl; // false (0)

// 检查 lambda, lambda 接受 string,const char* 可以构造 string,且返回 size_t
auto lambda = [](const std::string& s) { return s.size(); };
std::cout << "lambda(const char*): "
<< std::is_invocable_r_v<size_t, decltype(lambda), const char*> << std::endl; // true (1)


// ==========================================
// 2. invoke_result 测试
// ==========================================
std::cout << "\n--- invoke_result checks ---" << std::endl;

// 2.1 基硋试例:获取普通函数的返回类型
// 语法:std::invoke_result_t<Callable, Args...>
using FuncReturn = std::invoke_result_t<decltype(func), int>;
// 验证返回类型是否为 void
std::cout << "Return type of func(int) is void? "
<< std::is_same_v<FuncReturn, void> << std::endl; // true (1)

// 2.2 测试 lambda 的返回值
// lambda 的逻辑是:接受 string,返回 size_t
using LambdaReturn = std::invoke_result_t<decltype(lambda), std::string>;
// 验证返回类型是否为 size_t
std::cout << "Return type of lambda(string) is size_t? "
<< std::is_same_v<LambdaReturn, size_t> << std::endl; // true (1)

// 2.3 测试函数对象的返回值
// Functor 的 operator() 返回 void
using FunctorReturn = std::invoke_result_t<Functor, double>;
std::cout << "Return type of Functor(double) is void? "
<< std::is_same_v<FunctorReturn, void> << std::endl; // true (1)

return 0;
}

std::is_invocable 的第一个模板参数要求传入的是 Type(类型),而不是 Expression(表达式/变量)。

  • func 是一个标识符(虽然它代表函数地址)。
  • std::is_invocable< ... > 尖括号里必须填类型
  • decltype(func) 才是 func类型

所以,必须要用某种方式把 func 变成类型,或者手动写出类型。

2.4 类型修改

注意:这些不是 _v 变量,而是 type 成员。C++14/17 使用 _t 别名模板。

特性类 C++14/17 别名模板 描述
remove_const<T> remove_const_t<T> 移除 const
remove_volatile<T> remove_volatile_t<T> 移除 volatile
remove_cv<T> remove_cv_t<T> 移除 const 和 volatile
add_const<T> add_const_t<T> 添加 const
remove_reference<T> remove_reference_t<T> 移除引用
add_lvalue_reference<T> add_lvalue_reference_t<T> 添加左值引用 &
add_rvalue_reference<T> add_rvalue_reference_t<T> 添加右值引用 &&
remove_pointer<T> remove_pointer_t<T> 移除顶层指针
add_pointer<T> add_pointer_t<T> 添加指针
make_signed<T> make_signed_t<T> 转为有符号类型
make_unsigned<T> make_unsigned_t<T> 转为无符号类型
remove_extent<T> remove_extent_t<T> 移除数组的一维
remove_all_extents<T> remove_all_extents_t<T> 移除数组的所有维度
decay<T> decay_t<T> 类型退化:数组转指针、函数转指针、去 cv、去引用

2.5 特性逻辑组合

C++11 时只能写 std::is_integral::value && std::is_floating_point::value。C++17 提供了类型层面的逻辑运算。

特性类 描述
conjunction<B1...> 逻辑与。如果所有 Bi 都为 true,则为 true。短路求值(遇到 false 停止)。
disjunction<B1...> 逻辑或。如果任意 Bi 为 true,则为 true。短路求值。
negation<B> 逻辑非。取反。

定义一个类型特性,检查是否为数字类型。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <type_traits>

// 逻辑:是算术类型 且 不是 bool (bool也是算术类型,但在某些业务逻辑中我们排除它)
template<typename T>
using is_numeric = std::conjunction<std::is_arithmetic<T>, std::negation<std::is_same<T, bool>>>;

int main()
{
// 1. 使用 _t 别名模板
using CharPtr = std::add_pointer_t<char>;
std::cout << "CharPtr is pointer? " << std::is_pointer_v<CharPtr> << std::endl; // true

// 2. 使用 C++17 的 conjunction (逻辑与) 和 negation (逻辑非)
// 检查 double 是否符合我们自定义的 "numeric" 定义:是算术且不是bool -> true
std::cout << "Is double numeric? " << is_numeric<double>::value << std::endl; // true

// 检查 bool 是否符合
std::cout << "Is bool numeric? " << is_numeric<bool>::value << std::endl; // false

// 检查 CharPtr 是否符合
std::cout << "Is char* numeric? " << is_numeric<CharPtr>::value << std::endl; // false

return 0;
}