TMP
Template metaprogramming is a family of techniques to create new types and compute values at compile time. C++ templates are Turing complete if there are no limits to the amount of recursive instantiations and the number of allowed state variables. Erwin Unruh was the first to demonstrate template metaprogramming at a committee meeting by instructing the compiler to print out prime numbers in error messages. The standard recommends an implementation support at least 1024 levels of recursive instantiation, and infinite recursion in template instantiations is undefined behavior.
模板元编程(Template Metaprogramming,TMP)
- 概念:利用 C++ 模板在编译期执行计算和逻辑操作,将一些原本在运行时的计算提前到编译阶段完成,以提升运行效率。
- 应用:比如计算阶乘、斐波那契数列等数学运算,以及在编译期进行类型检查、类型转换、生成特定类型的容器等。在一些库(如 Boost.MPL 库)中,大量使用模板元编程来实现编译期的类型序列、类型算法等功能。
- 示例代码:计算编译期常量的阶乘
template <int N>
struct Factorial {
static const int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static const int value = 1;
};使用 Factorial<5>::value 就能在编译期得到 5 的阶乘值。
CRTP
定义
The Curiously Recurring Template Pattern is an idiom in which a class
Xderives from a class templateY, taking a template parameterZ, whereYis instantiated with Z = X.
For example,
template<class Z>
class Y {};
class X : public Y<X> {};原理
CRTP may be used to implement “compile-time polymorphism”, when a base class exposes an interface, and derived classes implement such interface.
#include <cstdio>
#ifndef __cpp_explicit_this_parameter // Traditional syntax
template <class Derived>
struct Base
{
// 基类通过静态转换调用派生类的实现
void interface() { static_cast<Derived*>(this)->impl(); }
protected:
Base() = default; // prohibits the creation of Base objects, which is UB
};
// 派生类的具体实现
struct D1 : public Base<D1> { void impl() { std::puts("D1::impl()"); }};
struct D2 : public Base<D2> { void impl() { std::puts("D1::impl()"); }};
#else // C++23 deducing-this syntax
struct Base { void name(this auto&& self) { self.impl(); } };
struct D1 : public Base { void impl() { std::puts("D1::impl()"); } };
struct D2 : public Base { void impl() { std::puts("D2::impl()"); } };
#endif
int main()
{
D1 d1; d1.name();
D2 d2; d2.name();
}
// Output:
D1::impl()
D2::impl()CRTP 中,基类是一个模板类,其模板参数是派生类本身。典型代码示例如下:
在上述代码里,Base 是模板基类,接受派生类 Derived 作为模板参数,而 Derived 类继承自 Base<Derived> ,将自身作为模板参数传递给了基类,形成了一种特殊的递归依赖关系。
核心原理
- 模板继承:
- 基类模板化:基类
Base被定义为模板类,能够接受不同的派生类类型作为模板参数。 - 派生类继承基类模板:派生类在继承时,把自身类型作为模板参数传递给基类模板,建立起一种特殊的继承关联。
- 基类模板化:基类
- 静态多态:
- 编译期绑定:基类中通过
static_cast<Derived*>(this),将this指针转换为派生类类型,直接调用派生类的方法。函数调用在编译期间就已经确定,无需运行时的动态查找。 - 无虚函数开销:与传统的通过虚函数表(vtable)实现的动态多态不同,CRTP 不需要虚函数表,从而避免了运行时间接跳转带来的开销。
- 代码复用与扩展:基类可定义通用逻辑,像示例中的
interface函数,而派生类负责提供具体的实现,比如implementation函数。当有新的派生类时,只需实现特定方法,不用修改基类代码,提升了代码的复用性和扩展性
- 编译期绑定:基类中通过
工作流程
- 模板实例化:当定义派生类(如
Derived类)时,编译器会对基类模板进行实例化,生成针对该派生类的基类代码,比如Base<Derived>。 - 方法调用:当调用
Base<Derived>::interface()时,通过static_cast将基类指针安全转换为派生类指针,进而直接调用Derived::implementation()方法。 - 编译期解析:所有的类型转换和函数绑定操作都在编译期完成,最终生成高效的机器码,运行时执行效率高。
起源与命名
- 起源:CRTP 的正式命名和识别可追溯到 1995 年,由 James Coplien 首次提出,他在观察早期 C++ 模板代码以及 Timothy Budd 在多范式语言 Leda 中创建的代码示例时发现了这种结构。 同年,Jan Falkin 在 Microsoft 的 Active Template Library (ATL) 开发过程中,也独立发现了 CRTP,他偶然将基类派生自派生类,实现了类似效果。
- 命名含义:
- “奇异(Curiously)”:在传统面向对象继承里,一般是基类固定,派生类扩展基类功能。但 CRTP 中基类是模板,派生类把自身作为参数传给基类,形成循环引用,这种设计在 C++ 社区里被认为反直觉。
- “递归(Recurring)”:派生类在继承层次结构中,以自身作为基类模板参数,从逻辑上看,类层次结构中出现了自身的引用,虽然编译时会解析,但体现出了类似递归的特征。
应用场景
- 库开发:被广泛应用于各种库开发,例如 Boost 库(包括 Boost.Iterator、Boost.Python 和 Boost.Serialization 等组件 ),以及 Windows 的 ATL(Active Template Library)和 WTL(Windows Template Library)库。
- 实现特定功能:C++ 标准库使用 CRTP 实现了
std::enable_shared_from_this,它允许类在共享指针上下文中安全地获取自身的共享指针。 - 性能敏感场景:在对性能要求极高的场景,如嵌入式系统、高频交易系统、流媒体处理、汽车域控制器等,由于能避免运行时虚函数调用开销,能极大提升程序运行效率。
与虚函数继承的对比
| 特性 | CRTP | 传统虚函数继承 |
|---|---|---|
| 多态类型 | 静态多态(编译时) | 动态多态(运行时) |
| 性能 | 高效,无运行时开销 | 有 vtable 开销,稍慢 |
| 灵活性 | 编译时确定,灵活性较低 | 运行时分派,灵活性较高 |
| 使用场景 | 性能敏感,库开发(如 ATL/WTL) | 需要运行时多态,复杂继承层次 |
| 虚函数需求 | 通常不需要 | 必须使用虚函数 |
类型萃取
类型萃取(Type Traits)是 C++ 中一项重要的高级编程技术,主要通过模板特化来获取类型的相关属性,并基于这些属性在编译期执行不同的逻辑,以下是详细介绍:
基本概念
在 C++ 编程中,不同类型具有不同的属性,如是否为整数类型、指针类型、数组类型等。类型萃取就是利用模板特化机制,编写一系列模板类来判断和提取这些类型属性,使得代码能够根据类型的不同特性,在编译阶段就选择合适的处理方式。
实现原理
- 模板与特化:通过定义一个通用的模板类,然后针对不同类型提供特化版本。例如,对于判断类型是否为整数的
std::is_integral,其基本实现结构如下:
// 通用模板
template <typename T>
struct is_integral {
static const bool value = false;
};
// 针对各种整数类型的特化
template <>
struct is_integral<char> {
static const bool value = true;
};
template <>
struct is_integral<short> {
static const bool value = true;
};
// 其他整数类型特化...在上述代码中,通用模板默认 value 为 false,而针对具体整数类型的特化版本将 value 设置为 true。这样在使用 std::is_integral<T>::value 判断类型 T 是否为整数时,编译器会根据 T 的实际类型选择通用模板或者对应的特化版本。
- 继承与组合:一些复杂的类型萃取工具会通过继承或组合的方式复用已有特性判断结果。比如
std::is_same用于判断两个类型是否相同,可能会基于其他基础的类型判断工具来构建逻辑。
标准库中的类型萃取工具
C++ 标准库在 <type_traits> 头文件中提供了丰富的类型萃取工具:
- 类型判断类:
std::is_integral:判断类型是否为整数类型(包括char、short、int、long等以及它们的无符号版本)。std::is_floating_point:判断类型是否为浮点数类型(如float、double、long double)。std::is_pointer:判断类型是否为指针类型。std::is_array:判断类型是否为数组类型。std::is_class:判断类型是否为类类型(不包括联合体、枚举、函数等)。
- 类型转换类:
std::remove_const:移除类型中的const限定符。例如std::remove_const<const int>::type的结果是int。std::remove_reference:移除类型中的左值引用或右值引用限定符。如std::remove_reference<int&>::type得到int。std::add_const:为类型添加const限定符。
- 条件判断与启用类:
std::enable_if:根据条件是否成立,决定模板函数或模板类是否参与重载决议。它有两种形式,一种用于函数返回值,一种用于函数模板的额外参数。例如:
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
processIntegral(T num) {
// 处理整数类型的逻辑
}上述代码中,只有当 T 是整数类型时,processIntegral 函数才会参与重载决议,避免了非整数类型调用该函数时可能出现的错误。
应用场景
- 模板函数重载决策:在模板函数中,根据传入参数的类型特性,选择不同的实现逻辑。例如,实现一个打印函数,对于整数类型和其他类型采用不同的打印方式:
#include <iostream>
#include <type_traits>
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
print(T num) {
std::cout << "Integral value: " << num << std::endl;
}
template <typename T>
typename std::enable_if<!std::is_integral<T>::value, void>::type
print(T obj) {
std::cout << "Non-integral value" << std::endl;
}- 优化算法实现:在算法实现中,根据容器元素的类型特性进行优化。比如,对于整数类型的容器排序,可以采用更适合整数的快速排序优化版本;对于浮点数类型容器,可以采用针对浮点数特点优化的排序算法。
- 自定义类型适配标准库:当自定义数据类型时,通过类型萃取相关技术,让自定义类型能够更好地适配标准库算法和容器。例如,自定义一个智能指针类型,通过类型萃取相关操作,使其能够像标准库智能指针一样参与类型判断和转换等操作。
与其他编程技术的结合
- 与模板元编程结合:类型萃取是模板元编程的重要组成部分,在模板元编程中,经常需要根据类型的特性进行编译期计算和逻辑控制。比如在编译期生成特定类型的容器,就需要先通过类型萃取判断类型是否满足要求。
- 与泛型编程结合:泛型编程旨在编写通用的代码,类型萃取可以帮助泛型代码在处理不同类型时,根据类型特性选择合适的处理方式,提升泛型代码的灵活性和适用性。例如,标准库中的算法在处理不同类型容器时,就借助类型萃取来实现针对不同类型的优化。