条款4:掌握查看型别推导结果的方法

ZaynPei Lv6

typeid介绍

typeid 是一个 C++ 内置的运算符(就像 sizeof 或 decltype 一样), 是 C++ 运行时类型识别(Runtime Type Information, RTTI)机制的核心组成部分。它允许你的程序在代码运行时查询一个对象的动态类型(Dynamic Type), 当然也可以进行静态类型的查询。

RTTI 并非没有代价(它会给对象增加额外的开销,主要是虚函数表 vtable 中的类型信息指针),因此大多数 C++ 编译器允许你通过选项(如 GCC/Clang 的 -fno-rtti)来禁用它。如果 RTTI 被禁用,typeid 在某些情况下(尤其是多态类型)将无法按预期工作。

具体来说, 它可以用于获取一个类型或一个表达式的类型信息。它有两种使用形式:

  • typeid(Type):直接传递一个类型名称, 如std::cout << (typeid(int).name()) << std::endl; // 输出 "int"
  • typeid(expression):传递一个表达式(例如一个变量名),下面主要考虑这一点 。

不过, typeid 的真正威力体现在它处理多态(Polymorphic)类型时的能力。一个类如果拥有至少一个 virtual 函数,它就是多态类型。

  1. 情况一:非多态类型(或静态类型查询)

当你对一个非多态类型(如 int、struct,或没有虚函数的类),或者对一个指针(ptr)本身(而不是它指向的内容 *ptr),或者对一个类型名称使用 typeid 时,它返回的是静态类型(Static Type)。

静态类型是对象在编译时被声明的类型。这个操作是在编译期完成的。

1
2
3
4
5
6
7
int x = 10;
std::string s = "hello";

// 这些都是静态类型查询
std::cout << (typeid(int).name()) << std::endl; // 输出 "int" (或其修饰名)
std::cout << (typeid(x).name()) << std::endl; // x 是 int,输出 "int"
std::cout << (typeid(s).name()) << std::endl; // 输出 "std::string" (或其修饰名)

  1. 情况二:多态类型(动态类型查询)

这是 typeid 最重要的用途。当你将 typeid 应用于一个多态类型的左值表达式(通常是对一个基类指针或引用的解引用)时,它会执行运行时查询,以确定该对象的动态类型(Dynamic Type)。

动态类型是对象在内存中被创建时的“真正”类型,即其派生最深的类型 (most derived type)。

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
#include <iostream>
#include <typeinfo> // 必须包含此头文件

struct Base {
virtual void foo() {} // 关键:必须有虚函数才能成为多态类型
virtual ~Base() {}
};

struct Derived : public Base {
void foo() override {}
};

struct OtherDerived : public Base {
void foo() override {}
};


int main() {
Base b_obj;
Derived d_obj;
Base* ptr_b = new Derived(); // 基类指针,指向派生类对象

// --- 静态类型 vs 动态类型 ---

// 1. ptr_b 本身是一个指针变量,变量类型是 Base* (非多态)
// 静态类型查询:
std::cout << (typeid(ptr_b).name()) << std::endl;
// 输出: "Base *" (或其修饰名,如 "P4Base")

// 2. *ptr_b 是对指针的解引用,这是一个多态类型的左值
// 动态类型查询:
std::cout << (typeid(*ptr_b).name()) << std::endl;
// 输出: "Derived" (或其修饰名,如 "7Derived")
// 尽管 ptr_b 是 Base*,RTTI 发现它实际指向一个 Derived 对象

delete ptr_b;
}

在这种情况下, 如果 typeid 运算符被应用于一个指向多态类型的空指针的解引用,它将抛出一个 std::bad_typeid 异常。

1
2
3
4
5
6
Base* null_b_ptr = nullptr;
try {
typeid(*null_b_ptr); // 试图解引用一个多态类型的空指针
} catch (const std::bad_typeid& e) {
std::cout << "Caught exception: " << e.what() << std::endl;
}

std::type_info 类

std::type_info:是一个定义在 头文件中的类。前面的 typeid 运算符的返回值就是一个对 std::type_info 对象的常量引用 (const std::type_info&), 这个对象中存储了关于特定类型的信息。

C++ 标准保证对于程序中的每一种类型,都只有一个 std::type_info 对象实例与之对应。这使得我们可以安全地使用 == 和 != 来比较它们。

std::type_info 类的主要成员函数包括:

  • operator== 和 operator!=:用于比较两个类型是否相同, 例如typeid(*ptr) == typeid(Derived)
  • name():返回一个 const char*,代表该类型的名称。
    • 值得注意的是, C++ 标准没有规定这个名称的具体格式,它只要求这个名称是唯一的。在实际中,大多数编译器会返回一个“修饰过的名称”(mangled name), 有时候并不易读(例如,7MyClass 而不是 MyClass)。
    • 这个函数主要用于调试和日志记录,不应该依赖它返回的字符串内容来进行程序逻辑判断(例如,不要写 if (strcmp(typeid(T).name(), “MyClass”) == 0))。

Boost

Boost 库是一个庞大、高质量、经过同行评审(peer-reviewed)且可移植的 C++ 库集合, 我们可以将其理解为 C++ 标准库的“扩展包”和“试验场”。许多在 Boost 库中经过多年开发、测试和广泛使用的组件,最终被 C++ 标准委员会采纳,并成为了 C++ 标准库的一部分, 例如: - 智能指针 (Smart Pointers): C++11 中的 std::shared_ptr 和 std::weak_ptr 直接源自 Boost 中的 boost::shared_ptr。

  • 线程库 (Threading): C++11 的 std::thread, std::mutex 等多线程工具,其设计和 API 大量借鉴了 Boost.Thread 库。

  • 可选值:C++17 的 std::optional 源于 boost::optional。

  • 类型特征 (Type Traits):C++11 头文件中的许多工具,最早在 Boost.TypeTraits 中实现。

  • 函数对象包装器:C++11 的 std::function 源于 boost::function。

Boost.TypeIndex

Boost.TypeIndex 是 Boost 库中的一个具体组件(子库),它提供了一套可移植的、功能更强的运行时类型识别(RTTI)机制,旨在作为 C++ 标准 RTTI(即 typeid 操作符和 std::type_info 类)的替代品或增强版。

为什么需要 TypeIndex?(标准 RTTI 的问题)

要理解 Boost.TypeIndex 的价值,首先必须了解 C++ 标准 typeid 的局限性:

  • 不可移植的类型名称:C++ 标准只规定 typeid(T).name() 必须返回一个字符串,但没有规定这个字符串的具体内容。在实际中,不同编译器返回的是“重整”(mangled)后的内部名称。例如,对于 std::vector, GCC/Clang 可能返回类似:St6vectorIiSaIiEE, 而MSVC 可能返回类似:.NSt3__16vectorIiNS_9allocatorIiEEEE. 这种字符串对开发者来说几乎不可读,且在不同平台和编译器之间完全不一致,导致调试和日志记录非常困难。

  • 对 RTTI 开关的依赖:typeid 的功能依赖于编译器开启 RTTI 选项。在许多高性能、游戏开发或嵌入式项目中,开发者会选择关闭 RTTI(例如在 GCC/Clang 上使用 -fno-rtti 标志)来减少二进制文件大小和潜在的虚函数表开销。而一旦 RTTI 被关闭,typeid 对多态类型(带有虚函数的类)的操作将失效(通常会导致编译错误或运行时异常),这使得依赖 typeid 的代码不具备健壮性。

  • CVR 限定符的丢失:标准的 typeid 在计算类型时,会自动忽略顶层的 const(常量)、volatile(易失)和 reference(引用)限定符(统称 CVR 限定符)。例如,typeid(int)、typeid(const int) 和 typeid(const int&) 这三者返回的 std::type_info 对象是完全相同的,它们都代表 int 类型。在某些需要精确类型区分的元编程或泛型编程场景中,这是致命的缺陷。

Boost.TypeIndex 的解决方案

Boost.TypeIndex 通过引入核心类 boost::typeindex::type_index 来解决上述所有问题:

  • 可移植的“美化”名称 (Pretty Name):Boost.TypeIndex 提供了 .pretty_name() 成员函数, 无论在哪个编译器上,也不论 RTTI 是否开启,它都会尽最大努力返回一个人类可读的、统一的类型名称。例如,对于 std::vector,它将一致地返回字符串 “std::vector<int, std::allocator >” (或类似的清晰形式),而不是混乱的重整名称。

  • 独立于 RTTI 开关:Boost.TypeIndex 具有智能的回退机制. 如果 RTTI 开启:它在内部优先使用 typeid,以获得最高效、最准确的多态类型识别; 如果 RTTI 关闭:它会自动切换到一套基于编译时模板元编程的机制来模拟类型信息(对于非多态类型)。这使得代码无论在哪种编译配置下都能工作。

  • 保留 CVR 限定符:Boost.TypeIndex 提供了两种获取类型信息的方式,以满足不同需求:

    • boost::typeindex::type_id():
      • 功能说明:这个函数模拟标准 typeid 的行为,返回去除 CVR 限定符后的基础类型。适用于需要“模糊”匹配类型的场景。
    • boost::typeindex::type_id_with_cvr():
      • 功能说明:这是关键的增强功能。它返回包含 CVR 限定符的精确类型。
    • 例如:type_id_with_cvr<const int&>() 返回的 type_index 对象的 .pretty_name() 将是 “int const&”,这与 type_id_with_cvr()(返回 “int”)是截然不同的,从而允许开发者进行精确的类型区分。

不同阶段用来查看编译器类型推导结果的技术

了解了上述内容之后, 我们可以总结出三种在软件开发不同阶段(撰写代码阶段、编译阶段和运行时阶段)用来查看编译器类型推导结果的技术。这些方法可以帮助开发者确认模板、auto 或 decltype 推导出的类型是否符合预期。

  1. IDE 编辑器

在IDE的代码编辑器中,将鼠标指针悬停在变量、参数或函数上时,编辑器通常会显示该实体的推导类型 。

其局限性在于, 这种方法需要代码基本处于可编译状态,因为IDE依赖内嵌的编译器前端来进行分析 。对于简单的类型(如int)显示良好,但对于复杂的类型,IDE显示的型别信息可能非常冗长且难以阅读,实用性会降低 。

  1. 编译器诊断信息

我们可以通过故意制造一个编译错误,来迫使编译器在诊断信息中报告出它所推导出的类型 。

一种方式是, 我们声明一个类模板,但不去定义它(例如 template<typename T> class TD;)。

接着尝试使用你想查看的类型来具现这个模板(例如,TD<decltype(x)> xType;) 。

编译器在试图创建 xType 对象时,会因为 TD 是一个不完整类型 (incomplete type) 而报错,错误信息中几乎必然会包含 T 被推导出的完整类型(例如 “aggregate ‘TD xType’ has incomplete type”)。

  1. 运行时输出

一种不太可靠的方法是使用typeid, 例如通过typeid(x).name() 来在运行时打印类型名称。然而, 根据C++标准,std::type_info::name 在处理类型时,会如同函数按值传递形参一样。这意味着它会忽略类型的引用(&)属性,并移除顶层的 const 和 volatile 修饰符(CVR丢失)。这会导致输出错误的类型,例如,一个 const Widget* const& 类型可能会被错误地报告为 const Widget*(详见条款1的情况2部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 模板函数定义
template<typename T>
void f(const T& param);

// 一个返回 std::vector 的工厂函数
std::vector<Widget> createVec();

// 使用工厂函数返回值初始化 vw
const auto vw = createVec();

if (!vw.empty()) {
// 调用模板函数
f(&vw[0]);
}

template<typename T>
void f(const T& param)
{
using std::cout;
cout << "T = " << typeid(T).name() << '\n'; // 显示 T 的类型
cout << "param = " << typeid(param).name() << '\n'; // 显示 param 的类型
}
上述结果的输出为:T = class Widget const * param = class Widget const * 而显而易见, 在模板 f 中,形参 param 被声明为 const T&。无论 T 被推导成什么,param 的类型都应该是在 T 的基础上增加了 const 和 & 限定符(或者根据引用折叠规则)。T 和 param(即 const T&)不可能是同一个类型。

出现这个问题的原因就在于std::type_info::name的腐化性(丢失CVR): C++ 标准规定,typeid 在返回 name() 之前,会像函数按值传递(pass-by-value)那样来处理它获取到的类型。

应用到我们的例子:根据原理, 我们推导出了T = const Widget*param = const (const Widget*) &. 对于 typeid(T), 它分析 const Widget*。这是一个指针类型,不是引用,它的 const 是底层的(指向 const),不是顶层的。因此,按值传递规则不会移除任何东西。

但是对于 typeid(param), 它分析 const Widget* const &, 首先 & 被移除,类型变为 const Widget* const (一个指向 const Widget 的 const 指针)。接着因为该类型有一个顶层的 const(即指针本身的常量性)。这个 const 被移除。结果是和T一样的const Widget* 。

更可靠的方法 (Boost.TypeIndex):

一个更准确的运行时解决方案是使用 Boost 库的 TypeIndex 。

使用 boost::typeindex::type_id_with_cvr<T>().pretty_name() 可以获取一个包含人类可读的、精确类型表示的字符串。其名称中的 “cvr” 表明它会保留 const、volatile 和引用饰词,从而提供准确的类型信息 。

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
#include <boost/type_index.hpp> // 引入 Boost.TypeIndex 库
#include <iostream>
using std::cout;

// 导入 Boost.TypeIndex 的核心函数,用于获取包含 const/volatile/& 的精确类型
using boost::typeindex::type_id_with_cvr;

template<typename T>
void f(const T& param)
{
cout << "T = "
<< type_id_with_cvr<T>().pretty_name()
<< '\n';

// 2. 显示 param 的类型
// 在这个函数签名中,param 的类型永远是 const T&。
cout << "param = "
<< type_id_with_cvr<decltype(param)>().pretty_name()
<< '\n';
}

int main()
{
int x = 10;
const int cx = 20;
const int& rx = x;

cout << "--- 调用 f(x) --- (x 是 int)\n";
f(x);
cout << "\n--- 调用 f(cx) --- (cx 是 const int)\n";
f(cx);
cout << "\n--- 调用 f(rx) --- (rx 是 const int&)\n";
f(rx);
cout << "\n--- 调用 f(42) --- (42 是右值)\n";
f(42);
return 0;
}