C++ 模板与泛型编程

1. 模板基础:函数模板与类模板

1.1 为什么需要模板

假设你要写一个函数来比较两个值的大小:

cpp 复制代码
int Max(int a, int b) { return a > b ? a : b; }
double Max(double a, double b) { return a > b ? a : b; }
std::string Max(std::string a, std::string b) { return a > b ? a : b; }

逻辑完全相同,只是类型不同。这就是代码重复 ,违反了 DRY(Don't Repeat Yourself)原则。模板的核心目的就是:让编译器帮你根据类型自动生成代码

1.2 函数模板

cpp 复制代码
template<typename T>
T Max(T a, T b) {
    return a > b ? a : b;
}

// 使用
int    x = Max(3, 5);           // 编译器推导 T = int
double y = Max(3.14, 2.71);     // 编译器推导 T = double
auto   z = Max<std::string>("hello", "world"); // 显式指定 T

关键概念

  • template<typename T> 声明了一个类型参数 Ttypenameclass 在这里完全等价,但推荐用 typename,因为 T 不一定是 class。
  • 模板实例化 :编译器看到 Max(3, 5) 时,会生成一个 int Max(int, int) 的具体函数。这个过程叫实例化(instantiation)。
  • 模板参数推导 :编译器可以从函数参数推导出 T 的类型,无需显式指定。
  • **显式指定模板参数:当然也可以通过<>**来显式指定模板参数进而实例化生成指定参数的函数。
1.2.1 多个模板参数
cpp 复制代码
template<typename T, typename U>
auto Add(T a, U b) -> decltype(a + b) {
    return a + b;
}
// 1、decltype语法:
// decltype 是 C++11 引入的关键字,作用很简单:给它一个表达式,它告诉你这个表达式的类型是什么。需要注意的是:decltype 是编译期的操作,它不会真的执行表达式,只是分析表达式的类型。
// decltype解决的问题:例如在上面的模板例子中,你经常不知道运算结果的类型是什么(int + double 结果是 double,int + int 结果是 int,string + string 结果是 string。你没法写死返回类型。),decltype 就是用来解决这个问题的------让编译器自己推导。

// 2、尾置返回类型语法(trailing return type)
// 如果上面的例子直接用decltype(a + b)来代替auto,即为decltype(a + b) Add(T a, U b); 会直接编译报错;原因是编译器从左往右读,读到decltype(a + b)时,参数a和b还没有被声明,编译器不认识他们。
// 为了解决这个"参数还没声明就要用"的问题,C++11 引入了尾置返回类型语法------把返回类型挪到参数列表后面:auto Add(T a, U b) -> decltype(a + b);  
// 	auto          →  占位符,告诉编译器"返回类型在后面"
// 	Add(T a, U b) →  参数列表(先声明参数)
// 	->            →  箭头,引出真正的返回类型,尾置返回类型的语法标记
// 	decltype(a+b) →  真正的返回类型(这里 a、b 已经声明了,可以用)


// C++14 起可以简化为
template<typename T, typename U>
auto Add(T a, U b) {
    return a + b;  // 返回类型自动推导
}

auto result = Add(3, 4.5);  // T=int, U=double, 返回 double
1.2.2 非类型模板参数

模板参数不一定是类型,也可以是值(必须是编译期常量):

cpp 复制代码
template<typename T, int N>
struct Array {
    T data[N];
    int size() const { return N; }
};

Array<int, 10> arr;  // 编译期确定大小为 10 的 int 数组

应用:固定大小的环形缓冲区、编译期确定的消息 ID 范围。

1.3 类模板

cpp 复制代码
template<typename T>
class Stack {
private:
    std::vector<T> data_;

public:
    void push(const T& value) {
        data_.push_back(value);
    }

    T pop() {
        T top = data_.back();
        data_.pop_back();
        return top;
    }

    bool empty() const {
        return data_.empty();
    }
};

// 使用
Stack<int> intStack;
intStack.push(42);
int val = intStack.pop();

Stack<std::string> strStack;
strStack.push("hello");

1.4 模板的默认参数

cpp 复制代码
// 类模板可以有默认参数
template<typename T, typename Container = std::vector<T>>
class Stack {
    Container data_;
public:
    void push(const T& value) { data_.push_back(value); }
    // ...
};

Stack<int> s1;                    // 使用默认的 vector<int>
Stack<int, std::deque<int>> s2;   // 使用 deque<int>

1.5 成员函数模板

类的成员函数也可以是模板,独立于类模板参数:

cpp 复制代码
class Converter {
public:
    template<typename Target, typename Source>
    Target convert(Source value) {
        return static_cast<Target>(value);
    }
};

Converter c;
int x = c.convert<int>(3.14);       // Target=int, Source=double(推导)
double y = c.convert<double>(42);    // Target=double, Source=int(推导)

关键理解 :模板不是函数或类本身,而是编译器生成函数或类的蓝图。只有在使用时(实例化时),编译器才会生成真正的代码。


2. 模板特化与偏特化

2.1 为什么需要特化

通用模板不一定适合所有类型。比如你写了一个通用的比较函数,但对于 C 风格字符串(const char*),你不想比较指针地址,而是比较字符串内容:

cpp 复制代码
template<typename T>
bool IsEqual(T a, T b) {
    return a == b;  // 对于 const char*,这比较的是指针地址!
}

这时就需要特化------为特定类型提供不同的实现。

2.2 全特化(Full Specialization)

为某个完全确定的类型提供特殊实现:

cpp 复制代码
// 主模板(Primary Template)
template<typename T>
bool IsEqual(T a, T b) {
    return a == b;
}

// 全特化版本:专门处理 const char*
template<>
bool IsEqual<const char*>(const char* a, const char* b) {
    return std::strcmp(a, b) == 0;
}

// 使用
IsEqual(3, 3);               // 走主模板
IsEqual("hello", "hello");   // 走 const char* 特化版本

语法要点

  • 一定要有主模板,再有特化版
  • template<> 表示这是一个全特化(模板参数列表为空,因为所有参数都被确定了)。
  • IsEqual<const char*> 明确指定了特化的类型。
2.2.1 类模板的全特化
cpp 复制代码
// 主模板
template<typename T>
class Serializer {
public:
    static std::string serialize(const T& value) {
        // 通用实现:假设 T 有 toString 方法
        return value.toString();
    }
};

// 全特化:int 类型
template<>
class Serializer<int> {
public:
    static std::string serialize(const int& value) {
        return std::to_string(value);
    }
};

// 全特化:std::string 类型
template<>
class Serializer<std::string> {
public:
    static std::string serialize(const std::string& value) {
        return "\"" + value + "\"";
    }
};

2.3 偏特化(Partial Specialization)

只确定部分 模板参数,或者对模板参数施加某种约束注意:函数模板不支持偏特化,只有类模板支持。

cpp 复制代码
// 主模板:两个参数
template<typename T, typename U>
class Pair {
public:
    T first;
    U second;
    void print() {
        std::cout << "Generic Pair: " << first << ", " << second << std::endl;
    }
};

// 偏特化:当两个参数类型相同时
template<typename T>
class Pair<T, T> {
public:
    T first;
    T second;
    void print() {
        std::cout << "Same-type Pair: " << first << ", " << second << std::endl;
    }
};

// 偏特化:当第二个参数是指针时
template<typename T, typename U>
class Pair<T, U*> {
public:
    T first;
    U* second;
    void print() {
        std::cout << "Pointer Pair: " << first << ", " << *second << std::endl;
    }
};

// 使用
Pair<int, double> p1{1, 2.0};     // 走主模板
Pair<int, int> p2{1, 2};          // 走 <T, T> 偏特化
int val = 42;
Pair<int, int*> p3{1, &val};      // 走 <T, U*> 偏特化,U=int,不是int*

2.4 偏特化的常见形式

cpp 复制代码
// 假设主模板如下:
template<typename T, typename U = void>
class Handler { /* 通用实现 */ };

// 形式 1:指针特化
template<typename T>
class Handler<T*> { /* 专门处理指针类型 */ };

// 形式 2:引用特化
template<typename T>
class Handler<T&> { /* 专门处理引用类型 */ };

// 形式 3:容器特化
template<typename T>
class Handler<std::vector<T>> { /* 专门处理 vector */ };

// 形式 4:多参数中固定部分
template<typename T>
class Handler<T, int> { /* 第二个参数固定为 int */ };

2.5 特化的匹配优先级

编译器选择模板实例时遵循「最特殊匹配」原则:

复制代码
全特化 > 偏特化 > 主模板

如果有多个偏特化都能匹配,但没有一个「比其他所有都更特殊」,编译器会报二义性错误

2.6 应用

cpp 复制代码
// 序列化框架:根据类型选择不同的序列化策略
template<typename T>
struct NetSerializer {
    // 主模板:要求类型自带 Serialize 方法
    static void Write(Buffer& buf, const T& value) {
        value.Serialize(buf);
    }
    static void Read(Buffer& buf, T& value) {
        value.Deserialize(buf);
    }
};

// 特化基本类型
template<>
struct NetSerializer<int32_t> {
    static void Write(Buffer& buf, const int32_t& value) {
        buf.WriteInt32(value);
    }
    static void Read(Buffer& buf, int32_t& value) {
        value = buf.ReadInt32();
    }
};

template<>
struct NetSerializer<std::string> {
    static void Write(Buffer& buf, const std::string& value) {
        buf.WriteUint16(static_cast<uint16_t>(value.size()));
        buf.WriteBytes(value.data(), value.size());
    }
    static void Read(Buffer& buf, std::string& value) {
        uint16_t len = buf.ReadUint16();
        value.resize(len);
        buf.ReadBytes(value.data(), len);
    }
};

// 偏特化:vector<T> 自动序列化
template<typename T>
struct NetSerializer<std::vector<T>> {
    static void Write(Buffer& buf, const std::vector<T>& vec) {
        buf.WriteUint16(static_cast<uint16_t>(vec.size()));
        for (const auto& item : vec) {
            NetSerializer<T>::Write(buf, item);  // 递归调用元素的序列化
        }
    }
    static void Read(Buffer& buf, std::vector<T>& vec) {
        uint16_t count = buf.ReadUint16();
        vec.resize(count);
        for (auto& item : vec) {
            NetSerializer<T>::Read(buf, item);
        }
    }
};

这样你可以直接 NetSerializer<std::vector<int32_t>>::Write(buf, myVec) 来序列化一个整数向量,编译器会自动组合 vector 偏特化和 int32_t 全特化。


3. 模板的编译模型与工程实践

3.1 模板代码必须在头文件中

这是初学者最容易踩的坑。普通函数可以在 .h 中声明、.cpp 中定义。但模板不行:

cpp 复制代码
// math_utils.h
template<typename T>
T Max(T a, T b);  // 只有声明

// math_utils.cpp
template<typename T>
T Max(T a, T b) { return a > b ? a : b; }  // 定义

// main.cpp
#include "math_utils.h"
int x = Max(3, 5);  // ❌ 链接错误!undefined reference to Max<int>

为什么会链接失败?

编译器编译 math_utils.cpp 时,没有看到任何对 Max<int> 的调用,所以不会实例化 Max<int>。编译 main.cpp 时,编译器看到了调用,知道需要 Max<int>,但看不到模板定义(只有声明),所以只生成了一个外部符号引用。链接时找不到 Max<int> 的定义,报错。

解决方案 1(最常用):把定义也放在头文件中

cpp 复制代码
// math_utils.h
template<typename T>
T Max(T a, T b) {
    return a > b ? a : b;
}

解决方案 2:显式实例化

如果你知道模板只会被少数几个类型使用,可以在 .cpp 中显式实例化:

cpp 复制代码
// math_utils.cpp
template<typename T>
T Max(T a, T b) { return a > b ? a : b; }

// 显式实例化 int 和 double 版本
template int Max<int>(int, int);
template double Max<double>(double, double);

这样链接器就能找到 Max<int>Max<double> 的定义。但如果有人用了 Max<float>,依然会链接失败。

3.2 extern template(C++11)

当一个模板在多个翻译单元(.cpp 文件)中被实例化为相同的类型时,每个 .cpp 都会生成一份实例。链接器会去重,但编译时间被浪费了。extern template 可以告诉编译器:「这个实例化别在这里做,别的翻译单元已经做了。」

cpp 复制代码
// widget.h
template<typename T>
class Widget {
public:
    void doSomething() { /* ... */ }
};

// 告诉其他翻译单元:不要在你那里实例化 Widget<int>
extern template class Widget<int>;

// widget.cpp
#include "widget.h"
// 在这里实例化一次
template class Widget<int>;

应用 :大型项目中的 Serializer<PlayerData>ObjectPool<Bullet> 这类频繁使用的模板实例化,用 extern template 能显著减少编译时间。

3.3 模板导致的编译时间问题

模板是编译期展开的,大量模板会导致:

  1. 编译时间膨胀 :每个使用模板的 .cpp 都要做实例化。
  2. 二进制膨胀:不同翻译单元中相同的模板实例会生成重复代码(链接器可去重但不总是完美)。
  3. 错误信息爆炸:模板错误信息非常长且难读。

实践建议

  • 不要在头文件中 include 过多的模板头文件。
  • 使用前向声明减少头文件依赖。
  • 复杂的模板逻辑,考虑用 Pimpl 或类型擦除(后面会讲)隐藏模板细节。
  • 使用 extern template 减少重复实例化。

3.4 阅读模板编译错误的技巧

模板错误信息通常很长。核心技巧:

  1. 从第一个错误开始看。后面的错误往往是连锁反应。
  2. required from here。这告诉你是哪行代码触发了模板实例化。
  3. note: candidate template ignored。这告诉你编译器尝试了哪些模板但都不匹配。
  4. static_assert 给出清晰的错误信息
cpp 复制代码
template<typename T>
void Process(T value) {
    static_assert(std::is_integral_v<T>, 
        "Process() 只接受整数类型,你传入的类型不是整数");
    // ...
}

4. 类型萃取 type_traits

4.1 是什么

<type_traits> 是 C++11 引入的标准库头文件,提供了一组编译期的类型查询和类型变换工具。你可以在编译期问这些问题:

  • 这个类型是整数吗?是浮点数吗?是指针吗?
  • 这个类型能不能拷贝?能不能移动?
  • 这个类型是不是某个类的子类?
  • 把这个类型的 const 去掉后是什么类型?

4.2 类型查询(Type Predicates)

类型查询工具都继承自 std::true_typestd::false_type,有一个静态成员 value

cpp 复制代码
#include <type_traits>

// 基本类型判断
std::is_integral<int>::value;         // true - int 是整数类型
std::is_integral<double>::value;      // false
std::is_floating_point<double>::value;// true
std::is_pointer<int*>::value;         // true
std::is_reference<int&>::value;       // true

// C++17 简写:加 _v 后缀,省略 ::value
std::is_integral_v<int>;             // true(推荐用这种写法)

// 是否可以 trivially copy(直接 memcpy 安全吗?)
std::is_trivially_copyable_v<int>;           // true
std::is_trivially_copyable_v<std::string>;   // false(string 有动态内存)

// 是否是某个类的子类
struct Base {};
struct Derived : Base {};
std::is_base_of_v<Base, Derived>;    // true
std::is_base_of_v<Derived, Base>;    // false

// 两个类型是否相同
std::is_same_v<int, int32_t>;        // true(在大多数平台上)
std::is_same_v<int, long>;           // 取决于平台

// 是否有默认构造函数
std::is_default_constructible_v<int>;            // true
std::is_default_constructible_v<std::mutex>;     // true
std::is_copy_constructible_v<std::unique_ptr<int>>; // false

4.3 类型变换(Type Transformations)

cpp 复制代码
// 去掉 const
using T1 = std::remove_const_t<const int>;    // int

// 去掉引用
using T2 = std::remove_reference_t<int&>;     // int
using T3 = std::remove_reference_t<int&&>;    // int

// 去掉 const 和引用(组合使用)
using T4 = std::remove_const_t<std::remove_reference_t<const int&>>;  // int

// decay:模拟函数参数传值时的类型退化
// 去掉引用、去掉顶层 const/volatile、数组退化为指针、函数退化为函数指针
// const:告诉编译器变量只读,禁止修改(编译期约束)
// volatile:告诉编译器变量会被意外修改,禁止优化(运行期约束)
using T5 = std::decay_t<const int&>;      // int
using T6 = std::decay_t<int[10]>;         // int*
using T7 = std::decay_t<int(double)>;     // int(*)(double)

// 条件类型选择
using T8 = std::conditional_t<true, int, double>;    // int
using T9 = std::conditional_t<false, int, double>;   // double

// 添加指针/引用
using T10 = std::add_pointer_t<int>;       // int*
using T11 = std::add_lvalue_reference_t<int>;  // int&

4.4 实际应用

应用 1:安全的 memcpy 序列化
cpp 复制代码
template<typename T>
void WriteToBuffer(Buffer& buf, const T& value) {
    if constexpr (std::is_trivially_copyable_v<T>) {
        // POD 类型:直接 memcpy,最快
        buf.Append(reinterpret_cast<const char*>(&value), sizeof(T));
    } else {
        // 非 POD 类型:调用自定义序列化方法
        value.Serialize(buf);
    }
}

// constexpr是c++11引入的核心关键字,核心作用是强制【编译期求值】,让编译器在编译阶段就计算出表达式的数值,而不是等到程序运行时。它是现代 C++ 性能优化、类型安全、模板元编程的基石,代码里的 if constexpr 就是它最经典的实战用法之一。

// constexpr 与 const:
// const作用是只读(不可修改),在运行期生效,变量值可以是运行时确定,只是不能改;
// constexpr作用强制编译器常量且天然就是const,只读不可修改,在编译期生效,强制在编译阶段求值,值是编译期常量。
// 例子:
// const:运行期只读,值可以是运行时输入
int x;
std::cin >> x;
const int a = x; // ✅ 合法,a 只读,但值是运行时确定
// constexpr:必须编译期能算出值
constexpr int b = x; // ❌ 编译报错!x 是运行时变量,编译期算不出
constexpr int c = 10 + 20; // ✅ 合法,编译期直接算出 30

// 作用:
// 1、修饰变量:强制编译期常量:
// 必须是编译期可计算的表达式
constexpr int BUF_SIZE = 1024;
constexpr double PI = 3.1415926;
constexpr int MAX(int a, int b) { return a > b ? a : b; }
constexpr int LIMIT = MAX(100, 200); // ✅ 编译期算出 200
// 被 constexpr 修饰的变量,天然就是 const,只读不可修改。
// 必须用编译期可计算的表达式初始化,否则直接编译报错

// 2、修饰函数:强制编译期可调用
// constexpr 函数的核心要求:输入是编译期常量时,输出也必须是编译期常量。
// constexpr 函数:编译期/运行期都能调用
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int f5 = factorial(5); // ✅ 编译期算出 120,无运行开销
int x;
std::cin >> x;
int fx = factorial(x); // ✅ 运行期正常调用,兼容运行时场景
// C++11 要求 constexpr 函数只能有一条 return 语句;C++14 放开限制,支持循环、分支等完整逻辑。
// 函数内部不能有无法在编译期执行的操作(如 std::cout、动态内存分配)

// 3、 修饰构造函数:编译期构造对象
// 让自定义类型可以在编译期创建实例,是现代 C++ 高性能编程的常用技巧:
struct Point {
    int x, y;
    constexpr Point(int a, int b) : x(a), y(b) {} // constexpr 构造函数
};
constexpr Point p(1, 2); // ✅ 编译期构造对象,p 是编译期常量

// 4、if constexpr:编译期分支(你代码里的核心用法)
// 这是 C++17 引入的编译期条件判断,是模板编程的「神技」,上面那段代码的核心:
if constexpr (编译期常量表达式) {
    // 分支1:条件为真时,保留这段代码,编译
} else {
    // 分支2:条件为假时,直接丢弃这段代码,不编译
}
// 普通 if 的问题:如果用普通 if,两个分支都会被编译。
// if constexpr 的优势:编译期根据 T 的类型,直接丢弃不满足条件的分支。

// 总结:constexpr 就是让编译器帮你在编译时干活,把能提前算的都算好,让程序运行更快、更安全;if constexpr 则是让模板代码能根据类型自动裁剪,实现「一份代码,最优适配所有类型」。
应用 2:编译期断言保障安全
cpp 复制代码
template<typename T>
class ObjectPool {
    static_assert(std::is_default_constructible_v<T>,
        "ObjectPool 要求类型 T 必须有默认构造函数");
    static_assert(!std::is_abstract_v<T>,
        "ObjectPool 不能存储抽象类(不能实例化)");

    // ...
};
// static_assert(常量表达式, "错误提示字符串");
// static_assert(常量表达式);
// 第一个参数必须是编译期可计算的常量表达式,不能是运行时变量、函数返回值。第二个参数必须是字符串字面量(c++11/14必须写,17以上可以不写)
// static_assert 是编译期断言,在编译阶段检查条件是否成立,不运行程序;如果条件不满足,直接编译报错并输出自定义提示信息。
// 它和运行时的assert完全不同,assert是程序运行时检查,不符合就崩溃报错;static_assert是程序编译时检查,直接编译失败。
// 常用于:类型检查(例如模板编程)、数值范围检查(版本)、数组/内存大小检查
// 注意:static_assert是编译期指令,不是可执行语句,它可以写在全局作用域,命名空间,类、函数里,完全不受函数限制。

// 编译期 vs 运行期,例如:
// ① static_assert(...)
// 编译阶段执行
// 编译器看到它就检查条件
// 不生成可执行代码
// 可以写在任何能放声明 / 定义的地方(全局、函数外、类里、函数里)
// ② EXPECT_EQ(...) / 普通代码
// 运行阶段执行
// 必须放在函数内部(比如测试函数、main 里)
// 不能写在全局 / 函数外面
应用 3:通用的哈希函数组合
cpp 复制代码
template<typename T>
size_t HashValue(const T& value) {
    if constexpr (std::is_integral_v<T>) {
        return std::hash<T>{}(value);
    } else if constexpr (std::is_floating_point_v<T>) {
        return std::hash<T>{}(value);
    } else if constexpr (std::is_enum_v<T>) {
        return std::hash<std::underlying_type_t<T>>{}(
            static_cast<std::underlying_type_t<T>>(value));
    } else {
        return value.GetHashCode();  // 自定义类型需要提供 GetHashCode
    }
}

4.5 自定义 type_trait

你可以自己写 type_trait。其原理就是模板特化:

cpp 复制代码
// 判断一个类型是否是我们游戏框架中的消息类型
// 主模板:默认不是
template<typename T>
struct is_game_message : std::false_type {};

// 特化:LoginReq 是
struct LoginReq { /* ... */ };
template<>
struct is_game_message<LoginReq> : std::true_type {};

// 特化:MoveReq 是
struct MoveReq { /* ... */ }; 
template<>
struct is_game_message<MoveReq> : std::true_type {};

// 使用
static_assert(is_game_message<LoginReq>::value, "LoginReq 是消息类型");
static_assert(!is_game_message<int>::value, "int 不是消息类型");

// 更优雅的方式:检测是否有某个特定的成员
// 利用 SFINAE(下一节会详细讲)
template<typename T, typename = void>
struct has_msg_id : std::false_type {};

template<typename T>
struct has_msg_id<T, std::void_t<decltype(T::MSG_ID)>> : std::true_type {};

4.6 常用 type_traits 速查表

trait 用途 示例
is_integral 是否是整数类型 int, char, bool, uint64_t
is_floating_point 是否是浮点数 float, double
is_arithmetic 是否是算术类型(整数或浮点) int, double
is_enum 是否是枚举 enum Color {...}
is_pointer 是否是指针 int*, void*
is_reference 是否是引用 int&, int&&
is_same 两个类型是否相同 is_same_v<int, int> = true
is_base_of A 是否是 B 的基类 is_base_of_v<Base, Derived>
is_trivially_copyable 能否安全 memcpy int yes, string no
is_default_constructible 有无默认构造
is_copy_constructible 能否拷贝构造 unique_ptr no
is_abstract 是否是抽象类 有纯虚函数的类
remove_const 去掉 const const intint
remove_reference 去掉引用 int&int
decay 完整类型退化 const int&int
conditional 编译期三元选择 conditional_t<cond, A, B>
underlying_type 枚举的底层类型 enum E : uint8_tuint8_t
void_t (C++17) 检测表达式合法性 SFINAE 辅助

5. SFINAE 与 std::enable_if

5.1 SFINAE 是什么

SFINAE 全称 Substitution Failure Is Not An Error (替换失败不是错误)。这是 C++ 编译器在进行模板参数推导/替换时的一条规则

当编译器用具体类型替换模板参数时,如果产生了无效的类型或表达式,编译器不会报错 ,而是默默地忽略这个模板,继续尝试其他候选模板。

举个例子:

cpp 复制代码
template<typename T>
typename T::value_type GetFirst(const T& container) {
    return container[0];
}

template<typename T>
T GetFirst(T value) {
    return value;
}

int x = GetFirst(42);  // 会发生什么?

编译器尝试第一个模板:用 int 替换 T,需要 int::value_type------但 int 没有 value_type!按照 SFINAE 规则,编译器不报错 ,而是忽略这个模板,转而尝试第二个模板。第二个模板匹配成功,调用 GetFirst<int>(42)

5.2 std::enable_if

std::enable_if 是 让编译器[有条件地启用/禁用]某个函数或模板,它是C++里的编译期开关:条件成立->这个函数有效,条件不成立->这个函数直接消失;它的作用:让不同类型走不同函数,不冲突,不报错。

std::enable_if<条件表达式,定义给type的类型(如果未指定默认就是void)>::type 或者C++17简写 std::enable_if_t<条件表达式>

cpp 复制代码
// 定义在头文件<type_traits>
// std::enable_if原型
template<bool B, class T = void>
struct enable_if;

// C++14简化写法,引入了别名模板std::enable_if_t,省去了typename 和 ::type 的繁琐写法
template<bool B, class T = void>
using enable_if_t = typename enable_if<B, T>::type;

规则:

  • 如果条件为true -> type是一个合法类型;
  • 如果条件为false -> type不存在,这个"不存在"利用的就是SFINAE机制(替换失败不是错误)。

例子:让「整数类型」走函数 1,「浮点类型」走函数 2

cpp 复制代码
#include <type_traits>

// 只有 T 是整数时,这个函数才生效
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
func(T val) {
    std::cout << "整数: " << val << std::endl;
}

// 只有 T 是浮点时,这个函数才生效
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
func(T val) {
    std::cout << "浮点: " << val << std::endl;
}

// 使用
func(10);      // 整数版本
func(3.14);    // 浮点版本

/*
	为什么不会冲突,因为编译器会:
		1、尝试匹配第一个函数
		2、条件不满足 → type 不存在 → 函数被丢弃
		3、再尝试第二个
		4、总有一个匹配
		5、这就是 enable_if 的魔力:自动选择正确的重载
*/

std::enable_if利用 SFINAE 原理条件编译的标准工具:

cpp 复制代码
// std::enable_if 的简化实现
// 主模板
template<bool Condition, typename T = void>
struct enable_if {};  // 条件为 false 时,没有 type 成员

// 偏特化,为什么采用偏特化的方式? 因为匹配规则是:全特化 > 偏特化 > 主模版,当第一个模板参数为true时,更加匹配偏特化版本,就走偏特化的逻辑,这样就达成了实现std::enable_if的逻辑
template<typename T>
struct enable_if<true, T> {
    using type = T;   // 条件为 true 时,有 type 成员
};

核心思想:当条件为 false 时,enable_if<false>::type 不存在,触发 SFINAE 让这个模板被排除。

用法 1:在返回类型上使用
cpp 复制代码
// 只有当 T 是整数类型时,这个函数才存在
template<typename T>
std::enable_if_t<std::is_integral_v<T>, T>
DoubleValue(T value) {
    return value * 2;
}

// 只有当 T 是浮点类型时,这个函数才存在
template<typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
DoubleValue(T value) {
    return value * 2.0;
}

DoubleValue(42);    // 调用整数版本
DoubleValue(3.14);  // 调用浮点版本
// DoubleValue("hello");  // 编译错误:没有匹配的函数(两个都被 SFINAE 排除了)
用法 2:在模板参数上使用(更推荐)
cpp 复制代码
// 更干净的写法:把 enable_if 放在模板参数中
template<typename T,
         std::enable_if_t<std::is_integral_v<T>, int> = 0>
void Process(T value) {
    std::cout << "整数处理: " << value << std::endl;
}

template<typename T,
         std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void Process(T value) {
    std::cout << "浮点数处理: " << value << std::endl;
}

这里的 int = 0 是一个匿名的非类型模板参数,默认值为 0 。当条件为 true 时,enable_if_t 产生 int 类型,这个参数合法。当条件为 false 时,enable_if_t 不存在,触发 SFINAE。

用法3:作为函数参数(较少用)

通过函数参数的默认值来触发SFINAE。

cpp 复制代码
// 示例:仅接受浮点数类型的函数
template <typename T>
void print_float(T t, typename std::enable_if<std::is_floating_point<T>::value>::type* = nullptr) {
    std::cout << "浮点数: " << t << std::endl;
}

int main() {
    print_float(3.14f);  // 正常调用
    // print_float(42);   // 编译错误:无匹配的重载
    return 0;
}

// 什么是多余参数?
// 多余参数 = 函数根本不用它、调用时也不用传、只是用来 "占位置" 实现特殊目的的参数。它在代码里存在,但没用,纯粹是为了满足语法 / 实现技巧。
// 例如:
void print_float(double t, void* = nullptr){}
// 第二个参数 void* = nullptr 就是多余参数。
// 多余参数函数体内用不着;调用时完全不用传参;对函数行为没有影响
// 对于普通函数,多余参数毫无意义,纯属多余;
// 对于模板+enable_if中,这个多余参数是用来触发SFINAE的工具,是实现类型约束的关键。

5.3 std::void_t 检测表达式合法性

std::void_t(C++17)是一个很巧妙的工具:无论给它什么模板参数,它的结果都是 void。如果参数中的某个表达式无效,整体替换失败,触发 SFINAE。

cpp 复制代码
// void_t 的定义极其简单
template<typename...>  // 1. 可变模板参数:接收任意数量、任意类型的模板参数
using void_t = void;   // 2. 别名模板:把所有输入都映射为 void

用它来检测类型是否拥有某个成员:

cpp 复制代码
#include <type_traits>

// 主模板:默认 T 没有 begin()
template<typename T, typename = void>
struct has_begin : std::false_type {};

// 偏特化版本:仅当 T 有 begin() 时,才会匹配这个版本
template<typename T>
struct has_begin<T, std::void_t<decltype(std::declval<T>().begin())>> 
    : std::true_type {};

// 这里模板的匹配规则解析:
// 前提:全特化 > 偏特化 > 主模版
// 情况 1:T = std::vector<int>(有 begin(),合法)
//   编译器尝试匹配偏特化版本:
//   计算 decltype(std::declval<T>().begin()):std::vector<int>::iterator,完全合法;
//   把这个类型传给 std::void_t<...>,void_t 成功实例化为 void;
//   偏特化版本的第二个模板参数是 void,和主模板的默认参数 void 匹配,偏特化优先级更高,所以最终 has_begin<std::vector<int>> 继承 std::true_type,value 为 true。
// 情况 2:T = int(没有 begin(),非法)
//   编译器尝试匹配偏特化版本:
//   计算 decltype(std::declval<int>().begin()):int 没有 begin() 成员,表达式非法;
//   传给 void_t 的参数非法,导致 void_t<...> 模板实例化失败;
//   触发 SFINAE:偏特化版本被排除,编译器只能匹配主模板;
//   最终 has_begin<int> 继承 std::false_type,value 为 false。

// 主模板:默认不能相加
template<typename T, typename U, typename = void>
struct can_add : std::false_type {};

// 偏特化:仅当 T + U 合法时匹配
template<typename T, typename U>
struct can_add<T, U, std::void_t<decltype(std::declval<T>() + std::declval<U>())>> 
    : std::true_type {};

// 测试
static_assert(can_add<int, double>::value);    // ✅ 合法,int+double 可以
static_assert(!can_add<std::string, int>::value); // ✅ 非法,string+int 不行

原理拆解:

  1. std::declval<T>() 创建一个 T 类型的"假对象"(不需要构造函数)。
  2. decltype(std::declval<T>().begin()) 获取调用 begin() 的返回类型。
  3. 如果 T 没有 begin() 方法,decltype(...) 会失败。
  4. std::void_t<...> 把失败传导为替换失败(SFINAE),退回到主模板(false_type)。
  5. 如果 Tbegin() 方法,void_t<...> 产生 void,偏特化匹配成功(true_type)。

void_t 相比 enable_if 的巧妙之处:

  1. enable_if 只能做布尔条件判断 (比如 std::is_integral<T>::value);
  2. void_t 可以做任意表达式的合法性判断 (比如 "有没有某个成员函数 / 成员变量 / 是否支持某种运算符",配合decltype和declval转换成类型提供给void_t),这是 enable_if 做不到的。

void_t 总结:

std::void_t 是 C++17 引入的SFINAE 辅助工具 ,它的本质是:用一个 "永远返回 void" 的模板,把「表达式是否合法」的问题,转化为「模板是否能成功实例化」的问题,从而触发 SFINAE,实现编译期的表达式合法性检测

5.4 应用

cpp 复制代码
// 消息处理器:只有注册了 MSG_ID 的类型才能作为消息
template<typename MsgType,
         std::enable_if_t<has_msg_id<MsgType>::value, int> = 0>
void RegisterHandler(std::function<void(Connection*, const MsgType&)> handler) {
    handlers_[MsgType::MSG_ID] = [handler](Connection* conn, const void* data) {
        handler(conn, *static_cast<const MsgType*>(data));
    };
}

// 如果传入一个没有 MSG_ID 的类型,编译期就报错
// RegisterHandler<int>(...);  // 编译失败:int 没有 MSG_ID

5.5 SFINAE vs if constexpr

在 C++17 以前,SFINAE 是实现编译期条件分支的主要方式。C++17 引入 if constexpr 后,很多场景可以用更简单的方式实现。但 SFINAE 在函数重载选择这个场景下依然不可替代:

cpp 复制代码
// if constexpr:只能在一个函数体内分支
template<typename T>
void Process(T value) {
    if constexpr (std::is_integral_v<T>) {
        // 整数逻辑
    } else {
        // 其他逻辑
    }
}

// SFINAE / enable_if:可以选择不同的函数重载
// 这在需要不同参数列表、不同返回类型的场景下更合适
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
int Process(T value) { return value * 2; }

template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
double Process(T value) { return value * 2.0; }

6. Tag Dispatch(标签分发)

6.1 是什么

Tag Dispatch 是一种利用函数重载 来实现编译期分支选择 的技术。核心思想是:用空结构体做"标签"(tag),通过函数重载来选择不同的实现

cpp 复制代码
// 定义标签
struct TrivialCopyTag {};
struct CustomCopyTag {};

// 实际实现:通过标签做重载
template<typename T>
void CopyImpl(T* dest, const T* src, size_t count, TrivialCopyTag) {
    // 可以安全 memcpy 的类型
    std::memcpy(dest, src, count * sizeof(T));
}

template<typename T>
void CopyImpl(T* dest, const T* src, size_t count, CustomCopyTag) {
    // 需要逐个拷贝构造的类型
    for (size_t i = 0; i < count; ++i) {
        new (dest + i) T(src[i]);
    }
}
 
// 对外接口:根据类型特征自动选择标签
template<typename T>
void Copy(T* dest, const T* src, size_t count) {
    using Tag = std::conditional_t<
        std::is_trivially_copyable_v<T>,
        TrivialCopyTag,
        CustomCopyTag
    >;
    CopyImpl(dest, src, count, Tag{});
}

6.2 与 SFINAE 的对比

Tag Dispatch 的优势在于可读性更好。对比:

cpp 复制代码
// SFINAE 方式(不太直观)
template<typename T, std::enable_if_t<std::is_trivially_copyable_v<T>, int> = 0>
void Copy(T* dest, const T* src, size_t count) { /* memcpy */ }

template<typename T, std::enable_if_t<!std::is_trivially_copyable_v<T>, int> = 0>
void Copy(T* dest, const T* src, size_t count) { /* 逐个构造 */ }

// Tag Dispatch 方式(清晰直观)
template<typename T>
void CopyImpl(T* dest, const T* src, size_t count, TrivialCopyTag) { /* memcpy */ }
template<typename T>
void CopyImpl(T* dest, const T* src, size_t count, CustomCopyTag)  { /* 逐个构造 */ }

6.3 标准库中的 Tag Dispatch

STL 中最经典的 Tag Dispatch 应用是迭代器分类

cpp 复制代码
// 标准库中的迭代器标签
struct input_iterator_tag {};
struct forward_iterator_tag : input_iterator_tag {};
struct bidirectional_iterator_tag : forward_iterator_tag {};
struct random_access_iterator_tag : bidirectional_iterator_tag {};

// std::advance 的实现(简化版)
template<typename It>
void advance_impl(It& it, int n, std::random_access_iterator_tag) {
    it += n;  // O(1):随机访问迭代器可以直接跳
}

template<typename It>
void advance_impl(It& it, int n, std::input_iterator_tag) {
    for (int i = 0; i < n; ++i) ++it;  // O(n):只能逐步前进
}

template<typename It>
void advance(It& it, int n) {
    // 用迭代器的 category 做标签分发
    advance_impl(it, n, typename std::iterator_traits<It>::iterator_category{});
}

6.4 应用

cpp 复制代码
// 不同类型的网络消息使用不同的发送策略
struct ReliableTag {};     // 可靠消息(TCP 语义)
struct UnreliableTag {};   // 不可靠消息(UDP 语义)
struct OrderedTag {};      // 有序消息

template<typename MsgType>
void SendImpl(Connection* conn, const MsgType& msg, ReliableTag) {
    // TCP 发送或 KCP 可靠通道
    conn->SendReliable(Serialize(msg));
}

template<typename MsgType>
void SendImpl(Connection* conn, const MsgType& msg, UnreliableTag) {
    // 原始 UDP 发送
    conn->SendUnreliable(Serialize(msg));
}

// 消息类型自带发送策略标签
struct MoveReq {
    using SendPolicy = UnreliableTag;  // 移动消息不需要可靠传输
    float x, y, z;
};

struct LoginReq {
    using SendPolicy = ReliableTag;    // 登录必须可靠
    std::string username;
};

template<typename MsgType>
void Send(Connection* conn, const MsgType& msg) {
    SendImpl(conn, msg, typename MsgType::SendPolicy{});
}


7. 万能引用与完美转发

7.1 引用折叠规则

在讲万能引用之前,需要先理解引用折叠(Reference Collapsing)。C++ 不允许"引用的引用"直接出现在代码中,但在模板实例化过程中会产生这种情况。编译器通过引用折叠规则来处理:

cpp 复制代码
// 引用折叠规则(记忆口诀:只要有左值引用参与,结果就是左值引用)
T& &   → T&    // 左值引用 + 左值引用 → 左值引用
T& &&  → T&    // 左值引用 + 右值引用 → 左值引用
T&& &  → T&    // 右值引用 + 左值引用 → 左值引用
T&& && → T&&   // 右值引用 + 右值引用 → 右值引用(唯一产生右值引用的情况)

只有右值引用 + 右值引用才能折叠出右值引用,其他任何组合都折叠为左值引用。这条规则是万能引用和完美转发的理论基石。

7.2 万能引用(Forwarding Reference)

T&& 出现在模板参数推导的上下文中 时,它不是右值引用,而是万能引用(也叫转发引用,Forwarding Reference):

cpp 复制代码
// 这是万能引用(T 是待推导的模板参数)
template<typename T>
void wrapper(T&& arg) { /* ... */ }

// 这不是万能引用,是普通的右值引用(类型已确定)
void func(int&& arg) { /* ... */ }

// 这也不是万能引用(虽然在模板类中,但 T 已在类实例化时确定)
template<typename T>
class Foo {
    void bar(T&& arg);  // 这是 T 的右值引用,不是万能引用
};

万能引用的推导规则:

cpp 复制代码
template<typename T>
void wrapper(T&& arg);

int x = 42;
const int cx = 42;

// 传入左值:T 推导为 左值引用类型
wrapper(x);         // T = int&,       T&& = int& && = int&       (引用折叠)
wrapper(cx);        // T = const int&, T&& = const int& && = const int&

// 传入右值:T 推导为 非引用类型
wrapper(42);        // T = int,        T&& = int&&
wrapper(std::move(x)); // T = int,    T&& = int&&

总结:传左值 → T 推导为左值引用 → 参数类型折叠为左值引用;传右值 → T 推导为非引用类型 → 参数类型为右值引用。 万能引用因此能同时接受左值和右值,名副其实的"万能"。

7.3 std::forward 完美转发

问题来了:万能引用虽然能接受任何值类别的参数,但函数参数本身永远是左值(因为它有名字、有地址)。如果直接把参数传给下一个函数,右值属性就丢失了:

cpp 复制代码
template<typename T>
void wrapper(T&& arg) {
    // arg 是一个左值(它有名字),无论外部传入的是左值还是右值
    innerFunc(arg);              // 永远以左值传递------右值属性丢失了!
    innerFunc(std::move(arg));   // 永远以右值传递------左值也被移动了,危险!
    innerFunc(std::forward<T>(arg));  // ✅ 完美转发:保持原始值类别
}

std::forward<T>(arg) 的作用是:如果原始参数是右值,就把 arg 转为右值;如果原始参数是左值,就保持 arg 为左值 。它依赖的就是 T 的推导结果和引用折叠规则:

cpp 复制代码
// std::forward 的简化实现原理
template<typename T>
T&& forward(std::remove_reference_t<T>& arg) {
    return static_cast<T&&>(arg);
}

// 当外部传入左值时:T = int&
// forward<int&>(arg) → static_cast<int& &&>(arg) → static_cast<int&>(arg) → 返回左值引用

// 当外部传入右值时:T = int
// forward<int>(arg)  → static_cast<int&&>(arg) → 返回右值引用

7.4 std::move 与 std::forward 的区别

这两个经常被混淆,但它们的语义完全不同:

cpp 复制代码
// std::move:无条件转为右值("我不要了,你拿去吧")
// 不管传入的是什么,都强制转为右值引用
template<typename T>
decltype(auto) move(T&& arg) {
    return static_cast<std::remove_reference_t<T>&&>(arg);
}

// std::forward:有条件转发("它来的时候是什么样,我原样传下去")
// 根据模板参数 T 的推导结果决定是左值还是右值
特性 std::move std::forward<T>
语义 无条件转为右值 有条件保持原始值类别
使用场景 明确表示要移动资源 模板中转发参数
需要模板参数 不需要 必须传 T
典型用法 return std::move(local); inner(std::forward<T>(arg));

7.5 典型应用模式

工厂函数(最经典的应用)
cpp 复制代码
// 完美转发工厂:参数原样传递给构造函数
template<typename T, typename... Args>
std::unique_ptr<T> MakeUnique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// 使用
auto p1 = MakeUnique<Player>("Alice", 100);  // 字符串字面量(右值)被转发
std::string name = "Bob";
auto p2 = MakeUnique<Player>(name, 200);     // name(左值)被转发,不会被移动
auto p3 = MakeUnique<Player>(std::move(name), 300); // 显式移动
包装函数 / 代理函数
cpp 复制代码
// 计时包装器:测量任意函数调用的耗时
template<typename Func, typename... Args>
decltype(auto) TimeIt(const std::string& label, Func&& func, Args&&... args) {
    auto start = std::chrono::high_resolution_clock::now();
    
    // decltype(auto) 保持返回值的精确类型(包括引用)
    decltype(auto) result = std::forward<Func>(func)(std::forward<Args>(args)...);
    
    auto elapsed = std::chrono::high_resolution_clock::now() - start;
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count();
    std::cout << "[" << label << "] " << ms << " ms" << std::endl;
    
    return result;
}

// 使用
auto player = TimeIt("CreatePlayer", MakePlayer, "Alice", 100);
游戏服务器应用:消息构造与发送
cpp 复制代码
class Connection {
public:
    // 用完美转发直接在发送缓冲区中构造消息,避免拷贝
    template<typename MsgType, typename... Args>
    void SendMessage(Args&&... args) {
        MsgType msg(std::forward<Args>(args)...);  // 原地构造
        auto buf = Serialize(msg);
        doSend(MsgType::MSG_ID, buf);
    }
};

// 使用
conn.SendMessage<MoveReq>(100.0f, 200.0f, 0.0f);  // 直接传参构造,无需先创建 MoveReq 对象

7.6 注意事项

  1. 不要对同一个对象多次 forward:forward 可能把对象转为右值,如果已经被移动,再次使用就是未定义行为。
cpp 复制代码
template<typename T>
void bad(T&& arg) {
    foo(std::forward<T>(arg));
    bar(std::forward<T>(arg));  // ❌ 危险!arg 可能已被移动
}

template<typename T>
void good(T&& arg) {
    foo(arg);                    // 先以左值使用
    bar(std::forward<T>(arg));   // 最后一次使用时再转发
}
  1. 万能引用只在模板参数推导时生效auto&& 也是万能引用(因为 auto 也涉及类型推导),但 vector<int>&& 不是(类型已确定)。
cpp 复制代码
auto&& val = someExpr;  // val 是万能引用
for (auto&& item : container) { /* ... */ }  // item 也是万能引用,range-for 的最佳实践

8. 变参模板 Variadic Templates

8.1 是什么

变参模板允许模板接受任意数量的参数:

cpp 复制代码
template<typename... Args>  // Args 是一个模板参数包(parameter pack)
void Print(Args... args);   // args 是一个函数参数包

... 在类型左边表示「打包」,在右边表示「展开」。

8.2 参数包的基本操作

cpp 复制代码
template<typename... Args>
void Info() {
    // sizeof...(Args) 返回参数包中的参数个数(编译期常量)
    std::cout << "参数个数: " << sizeof...(Args) << std::endl;
}

Info<int, double, char>();  // 输出: 参数个数: 3
Info<>();                   // 输出: 参数个数: 0

8.3 参数包展开方式

方式 1:递归展开(C++11 经典方式)
cpp 复制代码
// 递归终止条件(base case)
void Print() {
    std::cout << std::endl;
}

// 递归展开:每次处理第一个参数,剩余的递归处理
template<typename First, typename... Rest>
void Print(First first, Rest... rest) {
    std::cout << first << " ";
    Print(rest...);  // 递归调用,参数包逐步缩小
}

Print(1, 3.14, "hello", 'A');
// 展开过程:
// Print(1, 3.14, "hello", 'A')  →  打印 1,调用 Print(3.14, "hello", 'A')
// Print(3.14, "hello", 'A')     →  打印 3.14,调用 Print("hello", 'A')
// Print("hello", 'A')           →  打印 hello,调用 Print('A')
// Print('A')                    →  打印 A,调用 Print()
// Print()                       →  打印换行,结束
方式 2:逗号运算符展开(C++11 技巧)
cpp 复制代码
template<typename... Args>
void Print(Args... args) {
    // 利用逗号运算符在初始化列表中展开
    int dummy[] = { (std::cout << args << " ", 0)... };
    (void)dummy;  // 防止未使用变量警告
    std::cout << std::endl;
}

原理(expr, 0)... 会对参数包中的每个参数展开一次 expr,逗号运算符保证 expr 执行后返回 0,这些 0 用来初始化 dummy 数组。

方式 3:折叠表达式(C++17,下一节详讲)
cpp 复制代码
template<typename... Args>
void Print(Args... args) {
    ((std::cout << args << " "), ...);
    std::cout << std::endl;
}

8.4 变参类模板

cpp 复制代码
// 编译期类型列表
template<typename... Types>
struct TypeList {};

using GameMessages = TypeList<LoginReq, MoveReq, AttackReq, ChatReq>;

// 获取类型列表的长度
template<typename List>
struct TypeListSize;

template<typename... Types>
struct TypeListSize<TypeList<Types...>> {
    static constexpr size_t value = sizeof...(Types);
};

static_assert(TypeListSize<GameMessages>::value == 4);

8.5 完美转发与变参模板

变参模板配合完美转发是工厂函数和容器的 emplace 系列方法的基础:

cpp 复制代码
template<typename T, typename... Args>
std::unique_ptr<T> MakeUnique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// 使用:参数完美转发给 T 的构造函数
auto player = MakeUnique<Player>("Alice", 100, Vector3{0, 0, 0});

std::forward<Args>(args)... 展开方式

如果 Argsint, const string&, double,那么展开为:

cpp 复制代码
std::forward<int>(arg1), std::forward<const string&>(arg2), std::forward<double>(arg3)

每个参数都独立地转发,保持各自的左值/右值属性。

8.6 应用

事件系统

事件发射器是什么?可以理解为 EventEmitter = 一个万能的「消息中转站 + 广播喇叭」

举个例子:玩家被击杀 -> 要通知很多系统

  1. 日志系统要记录
  2. 界面系统要弹出提示
  3. 任务系统要判断是否完成
  4. 战斗系统要结算

如果不用发射器,写出来的代码强耦合:

复制代码
玩家死亡时:
    日志记录();
    弹出提示();
    任务更新();
    战斗结算();
// 耦合严重!改一个地方全要动。

如果使用发射器EventEmitter之后:

  1. 发射器 = 中间传话的人

    • 谁发生了事 → 告诉发射器
    • 谁关心这事 → 去发射器订阅
    • 发射器负责传话
  2. 三个角色

    1. 事件(Event) = 发生了什么事(事件)

      例:PlayerDeath(玩家死了)

    2. On (监听) = 谁想听这个事(监听事件)

      例:界面、日志、任务系统都来注册:

      "玩家死了记得叫我!"

    3. Emit (发射 / 触发) = 事情真的发生了(触发事件)

      告诉发射器:"玩家死啦!"

​ 发射器自动通知所有监听的人。

总的来说就是: EventEmitter 的意思 = 统一的消息广播中心, 让 "发生事情的人" 和 "处理事情的人" 互不认识,却能自动通信。

  • 谁要发消息 → 调用 Emit
  • 谁要收消息 → 调用 On
  • 中间不用互相认识、不用互相调用
  • 发射器负责自动转发
cpp 复制代码
// 泛型事件发射器
// 1、定义任意事件结构体(比如玩家死亡、玩家升级、物品拾取)
// 2、给事件注册回调函数(事件发生时要执行的逻辑)
// 3、触发事件时,自动调用所有注册的回调,并传递事件数据
// 4、完全类型安全:不同事件不会混淆,编译器自动校验类型
class EventEmitter
{
    // 基类处理器:统一包装所有事件回调
    using HandlerBase = std::function<void(const void *)>;
    // 事件映射表:key=事件类型ID,value=该类型所有回调函数列表
    // 按事件类型分组,存储各个类型事件的回调函数
    std::unordered_map<size_t, std::vector<HandlerBase>> handlers_;

public:
    // 注册事件处理器(用于注册)
    // EventType:事件类型
    // Handler &&handler:完美转发,保留传入lambda/函数的属性(左值/右值),避免拷贝开销
    template <typename EventType, typename Handler>
    void On(Handler &&handler)
    {
        // 1、获取事件类型的唯一ID(typeid运行时类型信息),同个事件类型ID也相同
        size_t typeId = typeid(EventType).hash_code();
        // 2、把用户传入的回调,包装成统一的handlerBase并存入容器
        handlers_[typeId].push_back([h = std::forward<Handler>(handler)](const void *data)
        { 
            // 3、回调函数执行时:void*转回真实的事件类型指针,调用用户函数
            h(*static_cast<const EventType *>(data)); 
        });
    }

    // 触发事件:用变参模板直接构造事件对象(用于触发)
    template <typename EventType, typename... Args>
    void Emit(Args &&...args)
    {
        // 1、直接用参数构造事件对象(无需手动new)
        EventType event(std::forward<Args>(args)...);
        // 2、获取事件类型ID
        size_t typeId = typeid(EventType).hash_code();
        // 3、查找该类型的所有回调
        auto it = handlers_.find(typeId);
        if (it != handlers_.end())
        {
            // 4、遍历所有回调,执行回调函数(传递事件对象)
            for (auto &handler : it->second)
            {
                handler(&event);
            }
        }
    }
};

// 使用
struct PlayerDeath
{
    uint64_t playerId;
    uint64_t killerId;
    PlayerDeath(uint64_t pid, uint64_t kid) : playerId(pid), killerId(kid) {}
};

int main()
{
    EventEmitter emitter;
    // 注册事件回调函数
    // 当事件触发后,显示击杀并回血
    emitter.On<PlayerDeath>([](const PlayerDeath &e) { std::cout << "Player " << e.playerId << " killed by " << e.killerId << std::endl; });
    emitter.On<PlayerDeath>([](const PlayerDeath &e) { std::cout << "Player " << e.playerId << " add " << 10 << " HP" << std::endl; });

    // 触发事件
    emitter.Emit<PlayerDeath>(1001, 2002);
    emitter.Emit<PlayerDeath>(1001, 2003);
    emitter.Emit<PlayerDeath>(1001, 2004);
}

// 关于typeid:
// typeid是C++ 运行时类型识别(RTTI) 关键字,用来获取一个类型 / 对象的「唯一身份标识」,你这段代码里用它就是为了:给每个事件类型生成一个唯一 ID,区分不同事件
// 例如代码中:typeid(EventType):拿到事件类型的类型信息(比如 PlayerDeath);.hash_code():生成一个数字 ID(size_t 整数);作用:每个类型对应唯一数字,当作哈希表的 key,让 PlayerDeath 的回调只处理 PlayerDeath 事件
// typeid的返回值:typeid(类型/对象) 返回一个 const std::type_info& 对象,里面存着这个类型的信息:
//     类型名字、唯一哈希值、类型大小等
// 两种用法:
//     typeid(类型).name():返回类型名字(比如 "PlayerDeath")
//     typeid(对象).hash_code():返回对象哈希值(比如 123456789)
// 核心能力:同类型一定相同,不同类型一定不同
//     typeid(int).hash_code() == typeid(int).hash_code()       // true,同类型
//     typeid(int).hash_code() != typeid(double).hash_code()    // true,不同类型
//     typeid(PlayerDeath).hash_code() != typeid(PlayerUpgrade).hash_code()  // true
// 特性:
//     1、编译期确认:对于基础类型(int、结构体),编译期就能确认类型。
//     2、运行时确认:对于多态类(带虚函数),运行时识别真实类型。
//     3、类型安全:不同类型不会混淆,编译器自动校验类型。
// typeid运行时识别:
//     典型列子:有类型Animal、Dog、Cat,基类时Animal,派生类时Dog、Cat
//     如果有代码:Animal* a = new Dog(); // 编译时编译器只知道a是Animal*,但实际上运行时真实类型是Dog*
//     这时候typeid就能识别出来:typeid(*a).name(); // 返回是"Dog";这就是运行时识别RTTI
日志格式化
cpp 复制代码
template<typename... Args>
void LogInfo(const char* fmt, Args&&... args) {
    // 将参数转发给 fmt 库或 spdlog
    spdlog::info(fmt, std::forward<Args>(args)...);
}

LogInfo("Player {} entered scene {} at ({}, {})", playerId, sceneId, x, y);

9. 折叠表达式 Fold Expressions(C++17)

9.1 是什么

折叠表达式是 C++17 引入的语法糖,让你可以用一个运算符把参数包中的所有元素"折叠"起来,无需递归。

9.2 四种折叠形式

cpp 复制代码
// 假设参数包 args 展开为 a, b, c, d
// 折叠展开的逻辑可以理解为:单个参数 op (剩余整体),然后剩余整体又可以是 单个参数 op (剩余整体)......这样循环进去,知道最后一个参数

// 1. 一元右折叠(Unary Right Fold)
(args op ...)        →  a op (b op (c op d))

// 2. 一元左折叠(Unary Left Fold)
(... op args)        →  ((a op b) op c) op d

// 3. 二元右折叠(Binary Right Fold)
(args op ... op init)  →  a op (b op (c op (d op init)))

// 4. 二元左折叠(Binary Left Fold)
(init op ... op args)  →  (((init op a) op b) op c) op d

9.3 实际示例

cpp 复制代码
// 求和
template<typename... Args>
auto Sum(Args... args) {
    return (args + ...);  // 一元右折叠
}
Sum(1, 2, 3, 4);  // 1 + (2 + (3 + 4)) = 10

// 带初始值的求和(防止空参数包)
template<typename... Args>
auto SafeSum(Args... args) {
    return (0 + ... + args);  // 二元左折叠,初始值为 0
}
SafeSum();  // 0(空参数包不会出错)

// 打印所有参数
template<typename... Args>
void Print(Args... args) {
    ((std::cout << args << " "), ...);  // 逗号运算符折叠
    std::cout << std::endl;
}

// 逻辑运算
template<typename... Args>
bool AllTrue(Args... args) {
    return (args && ...);  // 所有参数都为 true 才返回 true
}

template<typename... Args>
bool AnyTrue(Args... args) {
    return (args || ...);  // 有一个为 true 就返回 true
}

9.4 与逗号运算符结合

折叠表达式最强大的用法是和逗号运算符结合,可以对参数包中的每个元素执行任意操作:

cpp 复制代码
// 对每个参数执行一个操作
template<typename... Handlers>
void RegisterAll(Handlers&&... handlers) {
    (Register(std::forward<Handlers>(handlers)), ...);
    // 展开为:Register(h1), Register(h2), Register(h3), ...
}

// 把多个值插入容器
template<typename Container, typename... Values>
void InsertAll(Container& c, Values&&... values) {
    (c.push_back(std::forward<Values>(values)), ...);
}

std::vector<int> v;
InsertAll(v, 1, 2, 3, 4, 5);  // v = {1, 2, 3, 4, 5}

9.5 应用

cpp 复制代码
// 批量注册消息处理器
template<typename... MsgTypes>
void RegisterMessages() {
    (RegisterHandler<MsgTypes>(), ...);
}

RegisterMessages<LoginReq, MoveReq, AttackReq, ChatReq, UseItemReq>();

// 组合多个条件检查
template<typename... Validators>
bool ValidateAll(const PlayerAction& action, Validators... validators) {
    return (validators(action) && ...);
}

bool isValid = ValidateAll(action,
    CheckCooldown,
    CheckMana,
    CheckRange,
    CheckLineOfSight
);

10. if constexpr 编译期分支(C++17)

10.1 是什么

if constexpr 是编译期条件判断。与普通 if 不同,不满足条件的分支在编译期就被丢弃不会被编译。

cpp 复制代码
template<typename T>
std::string ToString(const T& value) {
    if constexpr (std::is_integral_v<T>) {
        return std::to_string(value);
    } else if constexpr (std::is_floating_point_v<T>) {
        char buf[32];
        snprintf(buf, sizeof(buf), "%.2f", static_cast<double>(value));
        return buf;
    } else if constexpr (std::is_same_v<T, std::string>) {
        return value;
    } else {
        // 兜底:要求类型有 toString() 方法
        return value.toString();
    }
}

10.2 为什么普通 if 做不到

因为对于普通的if,无论分支是否满足,每个分支都会进行编译,这就导致了:当需要根据模板参数的不同类型,来走不同的逻辑时,如果指定的类型不能同时兼顾其他逻辑分支的编译问题,就会编译报错(因为是所有分支逻辑都进行编译);

但if constexpr不同,不满足条件的分支在编译期就被丢弃,**不会被编译,**这也就意味着可以按照指定的类型写对应类型的操作就好了,无需兼顾其他逻辑分支的编译问题(因为不满足并不会被编译)

cpp 复制代码
template<typename T>
std::string ToString(const T& value) {
    if (std::is_integral_v<T>) {
        return std::to_string(value);  // 当 T=string 时,这行编译失败!
    } else {
        return value.toString();       // 当 T=int 时,这行编译失败!
    }
}

普通 if 的两个分支都会被编译 ,即使条件永远为 false。所以当 T=std::string 时,std::to_string(value) 不合法(to_string 不接受 string),编译失败。

if constexpr丢弃不走的分支,根本不编译它,所以不会有问题。

10.3 if constexpr 的规则

  1. 条件必须是编译期常量表达式
  2. 不满足条件的分支不会被实例化,但语法必须合法(除非在模板依赖的上下文中)。
  3. if constexpr 可以嵌套使用。
  4. if constexpr 可以有 else if constexprelse

10.4 经典应用场景

递归变参模板的终止
cpp 复制代码
// C++11 方式:需要单独的 base case 函数
void Print() {}  // base case
template<typename First, typename... Rest>
void Print(First first, Rest... rest) {
    std::cout << first << " ";
    Print(rest...);
}

// C++17 方式:if constexpr 替代 base case
template<typename First, typename... Rest>
void Print(First first, Rest... rest) {
    std::cout << first << " ";
    if constexpr (sizeof...(Rest) > 0) {
        Print(rest...);
    }
}
编译期选择不同的数据存储方式
cpp 复制代码
template<typename T>
class SmartBuffer {
    // 小对象放栈上,大对象放堆上
    static constexpr bool UseStack = sizeof(T) <= 64;

    // 编译期选择存储方式
    struct StackStorage { T data; };
    struct HeapStorage { std::unique_ptr<T> data; };

    std::conditional_t<UseStack, StackStorage, HeapStorage> storage_;

public:
    T& get() {
        if constexpr (UseStack) {
            return storage_.data;
        } else {
            return *storage_.data;
        }
    }
};

10.5 if constexpr vs SFINAE vs Tag Dispatch

技术 适用场景 优势 劣势
if constexpr 一个函数体内的分支选择 最简洁、最直观 不能改变函数签名(参数列表、返回类型)
SFINAE/enable_if 函数重载选择 可以有不同的返回类型和参数 语法晦涩
Tag Dispatch 基于类型特征的多路分发 可读性好、易扩展 需要定义标签类型
Concepts (C++20) 函数重载/模板约束 最清晰、错误信息最好 需要 C++20

实践建议 :在 C++17 中,优先用 if constexpr(最简洁)。如果需要不同的返回类型或参数列表,用 SFINAE。在 C++20 中,用 Concepts 替代 SFINAE。


11. CRTP 奇异递归模板模式

11.1 是什么

CRTP(Curiously Recurring Template Pattern)是 C++ 中最重要的模板设计模式之一。形式是:派生类把自己作为模板参数传给基类

cpp 复制代码
template<typename Derived>
class Base {
    // 基类"知道"自己的派生类是谁
};

class MyClass : public Base<MyClass> {
    // 把自己传给基类
};

看起来像递归(基类的参数是派生类,但派生类又继承自基类),所以叫「奇异递归」。

11.2 核心用途:静态多态

虚函数实现的是运行时多态 ,通过 vtable(虚函数表) 间接调用,有一定的性能开销(一次间接寻址 + 可能的 cache miss)。CRTP 实现的是编译期多态(静态多态),没有虚函数开销。

这里解释一下,vtable、vptr、vtable间接调用,为什么会有额外的性能开销(一次间接寻址 + 可能的cache miss)? vtable 全称 Virtual Function Table(虚函数表),vptr 全称 Virtual Pointer(虚函数表指针);这两个是C++实现**运行时多态(动态绑定)*的核心底层机制。vtable 是*编译器在编译阶段自动生成的、存储类虚函数地址的函数指针数组,可以理解为一张「虚函数导航表」。

  • 每个包含虚函数的类 ,会生成唯一的一张 vtable,同类的所有对象共享这张表(不是每个对象一份)
  • 表中按虚函数的声明顺序,存储该类所有虚函数的入口地址
  • 存储位置:程序的只读数据段(.rodata),防止运行时被意外修改

同时,编译器会给该类的每个对象 ,在内存布局的起始位置,插入一个隐藏的虚表指针(vptr,virtual pointer),这个指针指向所属类的 vtable,是对象和虚表的「桥梁」。

cpp 复制代码
// 运行时多态(虚函数)
class Shape {
public:
    virtual double area() const = 0; // 纯虚函数
    virtual ~Shape() = default;      // 虚析构函数

    void printArea() const {
        std::cout << "Area: " << area() << std::endl;  // 虚函数调用
    }
};

// 派生类 Circle,继承 Shape 并重写 area()
class Circle : public Shape {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double area() const override { return 3.14159 * radius_ * radius_; }
};

// 1. 编译阶段:生成 vtable
// 		Shape 类的 vtable:存储 Shape::area()(纯虚函数占位)、Shape::~Shape() 的地址
// 		Circle 类的 vtable:继承 Shape 的表结构,将 area() 条目替换为 Circle::area() 的地址,析构函数条目替换为 Circle::~Circle()
// 2. 对象创建:插入 vptr
// 		当你创建 Circle c(5.0); 时:
// 		对象 c 的内存开头,会被编译器插入一个 vptr,指向 Circle 类的 vtable
// 		基类 Shape 的指针 / 引用 Shape* s = &c; 会持有这个 vptr
// 3. 运行时:动态绑定(多态的核心)
// 		当执行 s->printArea() 时:
// 		printArea() 中调用 area(),编译器不会直接绑定 Shape::area()
// 		程序通过 s 指向对象的 vptr,找到 Circle 的 vtable
// 		从 vtable 中取出 area() 对应的函数指针,调用 Circle::area()
// 		最终输出圆的面积,实现「基类指针调用派生类函数」的多态效果

// 性能开销问题:
// 虚函数实现的是运行时多态,通过 vtable 间接调用,有一定的性能开销(一次间接寻址 + 可能的 cache miss)
// 我们拆解这两个开销:
// 		一次间接寻址:调用虚函数时,需要先通过 vptr 找 vtable,再从表中取函数指针,比普通函数的直接调用多了两次内存访问(取 vptr、取函数地址)
// 		Cache Miss 风险:vtable 存储在只读数据段,和对象内存(栈 / 堆)不连续,频繁跨段访问可能导致 CPU 缓存失效,降低性能
// 		这也是 CRTP(奇异递归模板模式)实现 编译期多态(静态多态) 的优势:没有 vtable 和 vptr,编译时直接绑定函数,无运行时开销。
// 编译期多态(CRTP)
template<typename Derived>
class Shape {
public:
    double area() const {
        // static_cast 在编译期完成,零运行时开销
        return static_cast<const Derived*>(this)->areaImpl();
    }

    void printArea() const {
        std::cout << "Area: " << area() << std::endl;  // 无虚函数调用
    }
};

class Circle : public Shape<Circle> {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double areaImpl() const { return 3.14159 * radius_ * radius_; }
};

class Rectangle : public Shape<Rectangle> {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double areaImpl() const { return w_ * h_; }
};

关键区别 :CRTP 方式中,Shape<Circle>Shape<Rectangle>不同的类型 ,不能放在同一个容器里(不像虚函数方式可以用 Shape*)。这就是编译期多态的代价------失去了运行时类型统一。

11.3 CRTP 的典型应用

应用 1:静态接口强制
cpp 复制代码
template<typename Derived>
class Serializable {
public:
    std::string serialize() const {
        return static_cast<const Derived*>(this)->doSerialize();
    }

    void deserialize(const std::string& data) {
        static_cast<Derived*>(this)->doDeserialize(data);
    }

    // 如果 Derived 没有实现 doSerialize,编译期就会报错
};

class PlayerData : public Serializable<PlayerData> {
public:
    std::string doSerialize() const {
        return "player_data";
    }
    void doDeserialize(const std::string& data) {
        // ...
    }
};
应用 2:实例计数器
cpp 复制代码
template<typename Derived>
class Counter {
    static inline int count_ = 0;  // C++17 inline 静态变量
public:
    Counter() { ++count_; }
    ~Counter() { --count_; }
    static int getCount() { return count_; }
};

// 每个派生类有自己独立的计数器
class Monster : public Counter<Monster> {};
class Bullet  : public Counter<Bullet> {};

// Monster::getCount() 和 Bullet::getCount() 是独立的
Monster m1, m2, m3;
Bullet b1;
std::cout << Monster::getCount();  // 3
std::cout << Bullet::getCount();   // 1

// 关于类内静态成员变量inline:
// 在 C++17 之前,类内的静态成员变量只能「声明」,不能「定义」,代码类似:
template<typename Derived>
class Counter {
    static int count_; // 仅声明,无初始化
};
// 必须在类外单独定义(分配内存)
template<typename Derived>
int Counter<Derived>::count_ = 0;
// ❌ 直接在类内写 static int count_ = 0; 是编译错误,因为这违反了「声明 / 定义分离」的规则。

// C++17 引入了 inline 静态成员变量,彻底改变了这个规则:允许在类内直接定义并初始化 inline static 成员变量,编译器会自动处理「单实例」问题,无需类外定义。

11.4 CRTP 的限制和注意事项

  1. 不能用基类指针做多态Shape<Circle>*Shape<Rectangle>* 是不同类型。如果需要运行时多态,还是用虚函数。
  2. 基类必须用 static_cast :不能用 dynamic_cast(没有虚函数表)。
  3. 防止误用:可以把基类的构造函数设为 private,只让 Derived 通过 friend 访问:
cpp 复制代码
template<typename Derived>
class Base {
    friend Derived;
    Base() = default;  // 只有 Derived 能构造
};

// 防止误写成 class Wrong : public Base<Other> {} // 这个牛

12. Mixin 模式:模板与继承的结合

12.1 是什么

Mixin 模式通过模板参数作为基类 ,实现功能的链式组合。与传统的多继承不同,Mixin 形成的是线性继承链,没有菱形继承问题。

cpp 复制代码
// Mixin:功能模块通过模板参数继承自上一个模块
template<typename Base>
struct WithLogging : Base {
    void log(const std::string& msg) {
        std::cout << "[LOG] " << msg << std::endl;
    }
};

template<typename Base>
struct WithTimestamp : Base {
    uint64_t getTimestamp() {
        return std::chrono::steady_clock::now().time_since_epoch().count();
    }
};

template<typename Base>
struct WithEncryption : Base {
    std::string encrypt(const std::string& data) {
        // 简单 XOR 加密示例
        std::string result = data;
        for (auto& c : result) c ^= 0x42;
        return result;
    }
};

// 基础连接类
struct RawConnection {
    void send(const std::string& data) {
        std::cout << "Sending: " << data << std::endl;
    }
};

// 组合!继承链:WithLogging -> WithEncryption -> WithTimestamp -> RawConnection
using SecureLoggedConnection = WithLogging<WithEncryption<WithTimestamp<RawConnection>>>;

SecureLoggedConnection conn;
conn.send("hello");         // 来自 RawConnection
conn.log("connected");      // 来自 WithLogging
conn.encrypt("secret");     // 来自 WithEncryption
auto ts = conn.getTimestamp(); // 来自 WithTimestamp

12.2 增强版:Mixin 覆盖基类行为

Mixin 的真正威力在于可以拦截和增强基类的行为

cpp 复制代码
struct RawConnection {
    void send(const std::string& data) {
        std::cout << "Raw send: " << data << std::endl;
    }
};

template<typename Base>
struct WithEncryption : Base {
    void send(const std::string& data) {
        std::string encrypted = data;
        for (auto& c : encrypted) c ^= 0x42;
        Base::send(encrypted);  // 调用基类的 send,形成处理链
    }
};

template<typename Base>
struct WithCompression : Base {
    void send(const std::string& data) {
        std::string compressed = "[compressed]" + data;
        Base::send(compressed);  // 调用基类的 send
    }
};

// 组合:发送时先压缩再加密
using MyConnection = WithCompression<WithEncryption<RawConnection>>;
// 调用链:WithCompression::send → WithEncryption::send → RawConnection::send

MyConnection conn;
conn.send("hello");
// 输出:Raw send: [compressed] 加密后的 hello

12.3 应用

cpp 复制代码
// 基础消息处理器
struct BasicHandler {
    void handleMessage(uint16_t msgId, const Buffer& data) {
        // 基础消息分发逻辑
        dispatch(msgId, data);
    }
};

// 加上频率限制
template<typename Base>
struct WithRateLimit : Base {
    std::unordered_map<uint16_t, int64_t> lastCallTime_;

    void handleMessage(uint16_t msgId, const Buffer& data) {
        auto now = GetTimeMs();
        if (now - lastCallTime_[msgId] < 100) {  // 100ms 内不能重复
            return;  // 丢弃
        }
        lastCallTime_[msgId] = now;
        Base::handleMessage(msgId, data);  // 通过检查,继续处理
    }
};

// 加上日志记录
template<typename Base>
struct WithAuditLog : Base {
    void handleMessage(uint16_t msgId, const Buffer& data) {
        LOG_INFO("Received msg: {}, size: {}", msgId, data.size());
        Base::handleMessage(msgId, data);
    }
};

// 组合:先日志 → 再限频 → 最后处理
using GameHandler = WithAuditLog<WithRateLimit<BasicHandler>>;

13. 类型擦除 Type Erasure

13.1 是什么

类型擦除是一种技术,让你在运行时隐藏具体类型信息 ,同时保持类型安全的接口。std::function 就是最经典的类型擦除例子------它可以持有任何可调用对象(函数指针、lambda、仿函数),而使用者不需要知道具体类型。

13.2 为什么需要

模板的问题是:不同的模板参数产生不同的类型 。你没法把 Handler<LoginReq>Handler<MoveReq> 放在同一个容器里。

cpp 复制代码
template<typename MsgType>
class Handler {
    void handle(const MsgType& msg);
};

// 想存在一起?做不到------它们类型不同
// std::vector<Handler<???>> handlers;  // ??? 填什么?

类型擦除解决这个问题:把模板参数"擦掉",让不同类型的对象可以统一存储和使用

13.3 类型擦除的实现原理

核心思想是:用非模板的基类/接口 + 模板派生类来桥接。

cpp 复制代码
// 第一步:定义非模板的接口
class IHandler {
public:
    virtual void handle(const void* data) = 0;
    virtual ~IHandler() = default;
};

// 第二步:模板派生类,持有具体的处理逻辑
template<typename MsgType>
class TypedHandler : public IHandler {
    std::function<void(const MsgType&)> func_;
public:
    TypedHandler(std::function<void(const MsgType&)> f) : func_(std::move(f)) {}

    void handle(const void* data) override {
        func_(*static_cast<const MsgType*>(data));
    }
};

// 现在可以统一存储了
std::unordered_map<uint16_t, std::unique_ptr<IHandler>> handlers;

// 注册
handlers[1001] = std::make_unique<TypedHandler<LoginReq>>(
    [](const LoginReq& req) { /* 处理登录 */ }
);
handlers[2001] = std::make_unique<TypedHandler<MoveReq>>(
    [](const MoveReq& req) { /* 处理移动 */ }
);

// 分发(msgId 和 data 来自网络层)
handlers[msgId]->handle(data);


// 例子:CRTP + Type Erasure(但是加了这一层,最顶层还是走的是继承多态的路,与CRTP相反)
class all
{
public:
    virtual double area() const = 0;
    virtual void printArea() const = 0;
};

template <typename Derived>
class Shape : public all
{
public:
    double area() const override
    {
        // static_cast 在编译期完成,零运行时开销
        return static_cast<const Derived *>(this)->areaImpl();
    }

    void printArea() const override
    {
        std::cout << "Area: " << area() << std::endl; // 无虚函数调用
    }
};

class Circle : public Shape<Circle>
{
    double radius_;

public:
    Circle(double r) : radius_(r) {}
    double areaImpl() const { return 3.14159 * radius_ * radius_; }
};

class Rectangle : public Shape<Rectangle>
{
    double w_, h_;

public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double areaImpl() const { return w_ * h_; }
};

int main()
{
    std::vector<std::unique_ptr<all>> vec;
    vec.push_back(std::make_unique<Circle>(5));
    vec.push_back(std::make_unique<Rectangle>(2, 3));
    for (const auto& p : vec)
    {
        p->printArea();
    }
    std::cout << "-" << std::endl;

    return 0;
}

13.4 手写简化版 std::function(模板 + 类型擦除)

理解 std::function 的内部实现,对理解类型擦除至关重要:

cpp 复制代码
// 下面这里例子建议先自己尝试着实现一个function,在实现的过程中会遇到一些问题,然后再尝试解决这些问题,最终就会得到下面类似的设计方法。

template<typename Signature>
class SimpleFunction;

template<typename Ret, typename... Args>
class SimpleFunction<Ret(Args...)> {
    // 非模板基类:定义接口
    struct Concept {
        virtual Ret invoke(Args... args) = 0;
        virtual ~Concept() = default;
    };

    // 模板派生类:适配任意可调用对象
    template<typename Callable>
    struct Model : Concept {
        Callable callable_;
        Model(Callable c) : callable_(std::move(c)) {}
        Ret invoke(Args... args) override {
            return callable_(std::forward<Args>(args)...);
        }
    };

    std::unique_ptr<Concept> impl_;

public:
    // 构造函数是模板:接受任意可调用对象
    template<typename Callable>
    SimpleFunction(Callable c) : impl_(std::make_unique<Model<Callable>>(std::move(c))) {}

    // 调用运算符不是模板:统一接口
    Ret operator()(Args... args) const {
        return impl_->invoke(std::forward<Args>(args)...);
    }
};

// 使用
SimpleFunction<int(int, int)> add = [](int a, int b) { return a + b; };
SimpleFunction<int(int, int)> mul = [](int a, int b) { return a * b; };
std::cout << add(3, 4);  // 7
std::cout << mul(3, 4);  // 12

精髓:构造函数是模板(知道具体类型),但存储和使用是非模板的(类型被"擦除"了)。

13.5 std::function 的性能代价

std::function 有两个性能代价:

  1. 堆分配 :要 new 一个 Model 对象(小对象优化 SBO 可以避免:在 std::function 对象内部(栈上) 预留一段固定大小、对齐良好的缓冲区(常见实现是 16~32 字节),如果要存储的可调用对象足够小 (比如无捕获 lambda、单个函数指针),直接用 placement new 把对象存到这个内部缓冲区,完全避免堆分配;如果对象太大 / 对齐不兼容,才回退到传统的堆分配。)。
  2. 虚函数调用:每次调用都通过虚函数间接调用。

性能敏感 的场景(比如游戏主循环中每帧调用的消息分发),可以考虑用模板函数指针替代 std::function

cpp 复制代码
// 轻量级替代方案:函数指针 + void* 上下文
using RawHandler = void(*)(void* ctx, const void* data);

struct HandlerEntry {
    RawHandler func;
    void* context;
};

// 适配模板:编译期生成类型安全的包装函数
template<typename MsgType, void(*Handler)(const MsgType&)>
void HandlerWrapper(void* ctx, const void* data) {
    Handler(*static_cast<const MsgType*>(data));
}

///////////////////////////////////详细例子///////////////////////////////////
#include <iostream>
#include <vector>

// 1. 定义通用函数指针类型
using RawHandler = void(*)(void* ctx, const void* data);

// 2. 处理器条目结构
struct HandlerEntry {
    RawHandler func;
    void* context;
};

// 3. 模板包装器
template<typename MsgType, void(*Handler)(const MsgType&)>
void HandlerWrapper(void* ctx, const void* data) {
    Handler(*static_cast<const MsgType*>(data));
}

// --------------------------
// 业务代码:消息类型 + 处理函数
// --------------------------
// 消息类型1:聊天消息
struct ChatMsg {
    std::string content;
    int from_uid;
};

// 消息类型2:登录消息
struct LoginMsg {
    std::string token;
    std::string device_id;
};

// 聊天消息处理函数
void OnChatMsg(const ChatMsg& msg) {
    std::cout << "收到聊天消息: " << msg.content << " (来自: " << msg.from_uid << ")" << std::endl;
}

// 登录消息处理函数
void OnLoginMsg(const LoginMsg& msg) {
    std::cout << "收到登录请求: token=" << msg.token << ", 设备=" << msg.device_id << std::endl;
}

// --------------------------
// 消息分发器
// --------------------------
class MessageDispatcher {
private:
    std::vector<HandlerEntry> handlers_;

public:
    // 注册处理器
    template<typename MsgType, void(*Handler)(const MsgType&)>
    void RegisterHandler() {
        // 用模板生成包装函数,注册到列表
        handlers_.push_back({
            &HandlerWrapper<MsgType, Handler>, // 函数指针
            nullptr // 上下文(这里是全局函数,传nullptr;成员函数传this)
        });
    }

    // 分发消息
    void Dispatch(int msg_type, const void* data) {
        // 简化示例:直接按索引分发(实际会用msg_type映射到对应handler)
        if (msg_type >= 0 && msg_type < handlers_.size()) {
            auto& entry = handlers_[msg_type];
            entry.func(entry.context, data); // 直接调用,无虚函数!
        }
    }
};

// --------------------------
// 测试
// --------------------------
int main() {
    MessageDispatcher dispatcher;
    // 注册处理器
    dispatcher.RegisterHandler<ChatMsg, &OnChatMsg>();
    dispatcher.RegisterHandler<LoginMsg, &OnLoginMsg>();

    // 模拟收到消息
    ChatMsg chat_msg{"你好", 1001};
    LoginMsg login_msg{"abc123", "iphone15"};

    // 分发消息(msg_type=0对应ChatMsg,1对应LoginMsg)
    dispatcher.Dispatch(0, &chat_msg);
    dispatcher.Dispatch(1, &login_msg);

    return 0;
}


14. 模板模板参数与策略模式

14.1 什么是模板模板参数

前面介绍的模板参数有两种:类型参数typename T)和非类型参数int N)。第三种是模板模板参数(Template Template Parameter)------模板参数本身是一个模板:

cpp 复制代码
// 普通类型参数:接受一个具体类型
template<typename Container>
class Wrapper1 {
    Container data_;  // Container 必须是完整类型,如 std::vector<int>
};

// 模板模板参数:接受一个模板(而不是类型)
template<template<typename> class Container>
class Wrapper2 {
    Container<int> intData_;       // 在这里才指定元素类型
    Container<double> doubleData_; // 同一个容器模板,不同元素类型
};

关键区别:typename Container 接受的是 std::vector<int> 这样的具体类型 ,而 template<typename> class Container 接受的是 std::vector 这个模板本身(未实例化的模板)。

14.2 基本语法

cpp 复制代码
// 语法:template<模板参数列表> class 参数名
// C++17 起也可以用 typename 替代 class
template<template<typename, typename> class Container, typename T>
class DataStore {
    Container<T, std::allocator<T>> storage_;
public:
    void add(const T& val) { storage_.push_back(val); }
    size_t size() const { return storage_.size(); }
};

// 使用:传入模板本身,不带尖括号
DataStore<std::vector, int> vecStore;    // 底层用 vector
DataStore<std::deque, std::string> dequeStore;  // 底层用 deque

vecStore.add(42);
dequeStore.add("hello");

注意:模板模板参数的参数列表必须和实际传入的模板参数个数兼容std::vector 实际上有两个模板参数(元素类型和分配器),所以上面用了 template<typename, typename> class Container。如果只写 template<typename> class Container,在一些编译器上会报错。C++17 起,如果模板有默认参数,可以省略。

14.3 策略模式的模板实现

模板模板参数最经典的应用是策略模式------让用户通过模板参数"注入"不同的策略,编译期绑定,零运行时开销:

cpp 复制代码
// ──── 策略定义 ────
// 分配策略
template<typename T>
struct HeapAllocator {
    T* allocate(size_t n) { return new T[n]; }
    void deallocate(T* p) { delete[] p; }
};

template<typename T>
struct PoolAllocator {
    T* allocate(size_t n) { return static_cast<T*>(pool_.alloc(n * sizeof(T))); }
    void deallocate(T* p) { pool_.free(p); }
private:
    static MemoryPool pool_;
};

// 日志策略
template<typename T>
struct ConsoleLogger {
    void log(const std::string& msg) {
        std::cout << "[Console] " << msg << std::endl;
    }
};

template<typename T>
struct FileLogger {
    void log(const std::string& msg) {
        // 写入文件...
    }
};

// ──── 使用策略的类 ────
template<typename T,
         template<typename> class AllocPolicy = HeapAllocator,
         template<typename> class LogPolicy = ConsoleLogger>
class ObjectPool : private AllocPolicy<T>, private LogPolicy<T> {
    T* buffer_ = nullptr;
    size_t capacity_ = 0;

public:
    ObjectPool(size_t cap) : capacity_(cap) {
        buffer_ = AllocPolicy<T>::allocate(cap);
        LogPolicy<T>::log("ObjectPool created with capacity " + std::to_string(cap));
    }

    ~ObjectPool() {
        AllocPolicy<T>::deallocate(buffer_);
        LogPolicy<T>::log("ObjectPool destroyed");
    }

    // ...
};

// 使用:像搭积木一样组合策略
ObjectPool<Bullet, HeapAllocator, ConsoleLogger> devPool(1000);   // 开发环境
ObjectPool<Bullet, PoolAllocator, FileLogger>    prodPool(10000); // 生产环境

与 Mixin 模式的区别:Mixin 模式中策略类继承自一个 Base 参数形成链式继承,每层可以拦截和增强基类行为;策略模式中策略类是独立的,被宿主类组合使用,各策略之间互不依赖。两者可以根据场景灵活选择。

14.4 不用模板模板参数的替代写法

有时可以用普通类型参数 + 嵌套类型来达到类似效果:

cpp 复制代码
// 方式 1:模板模板参数
template<template<typename> class Container, typename T>
class Store1 {
    Container<T> data_;
};

// 方式 2:普通类型参数 + 重新绑定(Rebind)
// 类似于标准库 allocator 的做法
template<typename Container>
class Store2 {
    Container data_;  // 直接使用传入的完整类型
};

// 使用
Store1<std::vector, int> s1;         // 模板模板参数
Store2<std::vector<int>> s2;         // 普通类型参数

// 模板模板参数的优势:可以在类内部用不同元素类型实例化同一个容器模板
// 普通类型参数的优势:语法更简单,兼容性更好

14.5 游戏服务器应用

cpp 复制代码
// 可配置的消息队列:序列化策略 + 容器策略
template<typename T>
struct JsonSerializer {
    static std::string serialize(const T& msg) { return toJson(msg); }
    static T deserialize(const std::string& data) { return fromJson<T>(data); }
};

template<typename T>
struct ProtobufSerializer {
    static std::vector<uint8_t> serialize(const T& msg) { return msg.SerializeAsString(); }
    static T deserialize(const std::vector<uint8_t>& data) {
        T msg; msg.ParseFromString(data); return msg;
    }
};

template<typename MsgType,
         template<typename> class Serializer = ProtobufSerializer>
class MessageQueue {
    std::queue<std::string> queue_;

public:
    void push(const MsgType& msg) {
        queue_.push(Serializer<MsgType>::serialize(msg));
    }

    MsgType pop() {
        auto data = queue_.front();
        queue_.pop();
        return Serializer<MsgType>::deserialize(data);
    }
};

// 开发时用 JSON(方便调试),上线用 Protobuf(高性能)
MessageQueue<ChatReq, JsonSerializer>      devQueue;
MessageQueue<ChatReq, ProtobufSerializer>  prodQueue;

15. std::tuple 与 std::index_sequence

15.1 std::tuple

std::tuple 是一个可以持有任意数量、任意类型 元素的编译期容器

std::tuple 是 C++11 引入的固定大小异构容器 ,可存储多个不同类型的元素。它是对 std::pair 的泛化(pair 只能存 2 个元素,tuple 可存任意数量),常用于函数返回多值、临时组合数据等场景。

定义与初始化:

cpp 复制代码
#include <tuple>
#include <string>

// 方式1:直接构造
std::tuple<int, double, std::string> t1(1, 3.14, "hello");

// 方式2:使用 std::make_tuple(自动推导类型)
auto t2 = std::make_tuple(42, 2.718, "world");

// 方式3:C++17 类模板参数推导(CTAD)
std::tuple t3(10, 3.14f, "test"); // 自动推导为 tuple<int, float, const char*>

访问元素std::get,通过**编译期索引(也就是说std::get中的N必须在编译时确定,不能用变量)**或类型访问元素(类型访问要求类型唯一):

cpp 复制代码
std::tuple<int, double, std::string> t(1, 3.14, "hello");

// 1. 通过索引访问(从 0 开始)
int i = std::get<0>(t);        // 获取第 0 个元素:1
double d = std::get<1>(t);     // 获取第 1 个元素:3.14

// 2. 通过类型访问(C++14 起,类型必须唯一)
std::string s = std::get<std::string>(t); // 获取 string 类型元素:"hello"
// 使用这个方法类型必须唯一,比如下面这个例子就是错误的,因为不唯一:
// std::tuple<int, std::string, double, std::string> t1(1, "hello", 1.2, "world");
// std::cout << std::get<std::string>(t); // 编译错误,因为不唯一

// 3. 修改元素(get 返回引用)
std::get<0>(t) = 100; // 修改第 0 个元素为 100

解包:std::tie 与结构化绑定: 将tuple元素解包到独立变量中:

cpp 复制代码
std::tuple<int, double, std::string> t(1, 3.14, "hello");

// 方式1:std::tie(C++11 起)
int a;
double b;
std::string c;
std::tie(a, b, c) = t; // 解包到 a, b, c
std::tie(a, std::ignore, c) = t; // 用 std::ignore 忽略某个元素

// 方式2:结构化绑定(C++17 起,更简洁)
auto [x, y, z] = t; // 直接绑定 x=1, y=3.14, z="hello"

获取tuple大小与元素类型

cpp 复制代码
using TupleType = std::tuple<int, double, std::string>;

// 1. 获取元素数量(编译期常量)
constexpr size_t size = std::tuple_size_v<TupleType>; // 3

// 2. 获取第 N 个元素的类型
using FirstType = std::tuple_element_t<0, TupleType>; // in

合并tuple:std::tuple_cat :

cpp 复制代码
std::tuple<int, double> t1(1, 3.14);
std::tuple<std::string, bool> t2("hello", true);

auto t3 = std::tuple_cat(t1, t2); 
// t3 类型为 std::tuple<int, double, std::string, bool>

遍历tuple:std::apply(C++17起):由于tuple元素类型不同,需要模板或std::apply进行遍历:

cpp 复制代码
#include <iostream>
#include <tuple>

std::tuple t(1, 3.14, "hello");

// 用 lambda + 折叠表达式遍历
std::apply([](auto&&... args) {
    ((std::cout << args << " "),...); // 输出:1 3.14 hello
}, t);

作为函数返回值(最常用场景):替代传统的"输出参数"方式,更优雅地返回多值:

cpp 复制代码
#include <tuple>
#include <string>
// 返回多个值
std::tuple<int, double, std::string> getUser() {
    return {1001, 98.5, "Alice"}; // C++17 可省略 make_tuple
}
// 使用
auto [id, score, name] = getUser(); // 结构化绑定接收
  1. 注意: 索引必须是编译期常量,std::get<N> 中的 N 必须在编译时确定,不能用变量

  2. std::make_tuple的类型退化:make_tuple 会对参数进行类型退化 (如数组退化为指针、引用退化为值)。若需保留引用,用 std::ref/std::cref

  3. 比较操作(C++20前用std::tie):tuple 默认支持按字典序比较(==, !=, <, <=, >, >=)。自定义类型比较时,常用 std::tie 简化:

    cpp 复制代码
    struct Person {
        std::string name;
        int age;
        
        bool operator<(const Person& other) const {
            // 按 name 比较,name 相同则按 age 比较
            return std::tie(name, age) < std::tie(other.name, other.age);
        }
    };

15.2 std::index_sequence

std::index_sequence 是一个编译期整数序列 ,通常配合 std::make_index_sequence 生成:

cpp 复制代码
// std::make_index_sequence<5> 生成 std::index_sequence<0, 1, 2, 3, 4>

它的核心用途是:在编译期"循环"遍历 tuple 或参数包

15.3 遍历 tuple

cpp 复制代码
// 打印 tuple 的所有元素
template<typename Tuple, size_t... Is>
void PrintTupleImpl(const Tuple& t, std::index_sequence<Is...>) {
    ((std::cout << std::get<Is>(t) << " "), ...);  // C++17 折叠表达式
}

template<typename... Args>
void PrintTuple(const std::tuple<Args...>& t) {
    PrintTupleImpl(t, std::make_index_sequence<sizeof...(Args)>{});
}

auto t = std::make_tuple(42, 3.14, "hello");
PrintTuple(t);  // 输出: 42 3.14 hello

原理

  1. sizeof...(Args) 得到 tuple 元素个数 = 3。
  2. std::make_index_sequence<3> 生成 std::index_sequence<0, 1, 2>
  3. PrintTupleImpl 的模板参数 Is... 推导为 0, 1, 2
  4. 折叠表达式展开为 std::get<0>(t), std::get<1>(t), std::get<2>(t)

15.4 std::apply

C++17 提供了 std::apply可以将 tuple 的元素展开为函数参数:

cpp 复制代码
auto args = std::make_tuple(1, 2.0, std::string("hello"));

auto result = std::apply([](int a, double b, const std::string& c) {
    std::cout << a << ", " << b << ", " << c << std::endl;
    return a;
}, args);

15.5 服务器应用:编译期组件类型注册

cpp 复制代码
// ECS 中:根据组件类型列表自动注册
template<typename... Components>
class ComponentRegistry {
    using ComponentTuple = std::tuple<std::vector<Components>...>;
    ComponentTuple storage_;

public:
    // 获取指定组件类型的存储
    template<typename C>
    std::vector<C>& getStorage() {
        return std::get<std::vector<C>>(storage_);
    }

    // 对所有组件存储执行操作
    template<typename Func>
    void forEachStorage(Func&& func) {
        std::apply([&func](auto&... storages) {
            (func(storages), ...);
        }, storage_);
    }
};

ComponentRegistry<PositionComponent, HealthComponent, VelocityComponent> registry;
registry.getStorage<PositionComponent>().push_back({1.0f, 2.0f, 3.0f});

16. 模板元编程基础 TMP

16.1 是什么

模板元编程(Template Metaprogramming)是利用模板机制在编译期进行计算的技术。C++ 模板系统本质上是一个图灵完备的编译期计算系统。

这里有两个核心关键点:

  1. 编译期执行 :TMP 的所有逻辑、计算、类型处理,都在代码编译阶段完成,运行期直接使用编译期生成的结果没有任何运行时计算开销

  2. 图灵完备:意味着模板系统可以实现任何逻辑(循环、分支、递归、变量 / 类型存储),理论上能在编译期完成任何可计算的任务。

普通 C++ 代码 模板元编程 TMP
运行期执行 编译期执行
操作数值、变量 操作类型、编译期常量
运行期分支、循环 编译期通过模板特化、递归实现分支与循环
运行期报错 编译期报错,提前发现问题

16.2 编译期计算

编译期计算是 TMP 最基础的能力:把原本运行期的数值计算,提前到编译期完成,生成编译期常量,运行期直接使用,零开销。

cpp 复制代码
// 编译期阶乘(经典示例)
template<int N>
struct Factorial {
    static constexpr int value = N * Factorial<N - 1>::value;
};

template<>
struct Factorial<0> {
    static constexpr int value = 1;
};

constexpr int result = Factorial<5>::value;  // 120,编译期计算完成

// C++14 以后,用 constexpr 函数更简洁,编译期计算完成
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);
}

编译期计算和运行时计算最直观的比较:

cpp 复制代码
// 测试代码

#include <iostream>
#include <chrono>
#include <utility>

// ====================== 编译期模板阶乘 ======================
template <long long N>
struct Fac
{
    static constexpr long long value = N * Fac<N - 1>::value;
};

template <>
struct Fac<0>
{
    static constexpr long long value = 1;
};

template <size_t... Is>
void allImpl(std::index_sequence<Is...>)
{
    // 使用折叠表达式访问所有编译期常量,volatile 防止被优化掉
    volatile long long sink;
    ((sink = Fac<Is>::value), ...);
}

template <long long N>
void all()
{
    allImpl(std::make_index_sequence<N>{});
}

// ====================== 普通运行时阶乘函数======================
long long fac_normal(long long n)
{
    long long res = 1;
    for (long long i = 1; i <= n; ++i)
    {
        res *= i;
    }
    return res;
}


void all_normal()
{
    volatile long long sink;
    for (int i = 0; i < 20; ++i)
    {
        sink = fac_normal(i);
    }
}

// ====================== 测试主函数 ======================
int main()
{
    const int test_times = 10000000; // 各调用 10000000 次

    // -------------------- 测试 编译期模板版本 --------------------
    auto start1 = std::chrono::high_resolution_clock::now();

    for (int i = 0; i < test_times; ++i)
    {
        all<20>();
    }

    auto end1 = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::milli> duration1 = end1 - start1;

    // -------------------- 测试 普通函数版本 --------------------
    auto start2 = std::chrono::high_resolution_clock::now();

    for (int i = 0; i < test_times; ++i)
    {
        all_normal();
    }

    auto end2 = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::milli> duration2 = end2 - start2;

    // ====================== 输出结果 ======================
    std::cout << "\n==================== 耗时对比 ====================\n";
    std::cout << "编译期模板版本 调用 " << test_times << " 次耗时:" << duration1.count() << " ms\n";
    std::cout << "普通运行时函数 调用 " << test_times << " 次耗时:" << duration2.count() << " ms\n";
    std::cout << "====================================================\n";

    return 0;
}

// 结果:
ubuntu$ ./test 

==================== 耗时对比 ====================
编译期模板版本 调用 10000000 次耗时:8.88274 ms
普通运行时函数 调用 10000000 次耗时:1902.71 ms
====================================================
ubuntu$ ./test 

==================== 耗时对比 ====================
编译期模板版本 调用 10000000 次耗时:8.62768 ms
普通运行时函数 调用 10000000 次耗时:1900.22 ms
====================================================
ubuntu$ ./test 

==================== 耗时对比 ====================
编译期模板版本 调用 10000000 次耗时:9.43706 ms
普通运行时函数 调用 10000000 次耗时:1922.13 ms
====================================================

对于编译期计算(模板元编程 + constexpr)优化的个人看法总结: 1、利用 C++ 模板编译期实例化 + constexpr 常量求值的特性,可对高频调用、输入编译期可确定、输入与输出为纯映射关系 的计算逻辑,在编译阶段提前完成全量运算,实现运行时零计算开销,相比普通运行时函数可带来数量级的性能提升。 2、不可突破的适用边界 :该技术完全不适用于运行时动态变化的变量场景 。核心原因是动态输入无法在编译期完成模板实例化,不仅无法实现预计算收益,还会导致模板实例化爆炸、编译成本陡增,无任何运行时优化价值。 3、编译期模板方案 :所有阶乘结果在编译阶段就已完成全量计算,直接以常量形式硬编码进二进制文件。运行时仅需一条mov指令完成常量赋值,无任何循环、乘法运算,真正实现了运行时零计算开销。 4、普通运行时函数方案:每次调用都需要 CPU 实打实执行循环 + 乘法指令,100 万次调用就会重复 100 万次完整的阶乘计算,所有计算开销完全集中在运行时。

16.3 编译期类型列表

cpp 复制代码
// 类型列表定义
template<typename... Types>
struct TypeList {};

// 获取长度
template<typename List>
struct Length;

template<typename... Types>
struct Length<TypeList<Types...>> {
    static constexpr size_t value = sizeof...(Types);
};

// 获取第 N 个类型
template<size_t N, typename List>
struct TypeAt;

template<size_t N, typename Head, typename... Tail>
struct TypeAt<N, TypeList<Head, Tail...>> {
    using type = typename TypeAt<N - 1, TypeList<Tail...>>::type;
};

template<typename Head, typename... Tail>
struct TypeAt<0, TypeList<Head, Tail...>> {
    using type = Head;
};

// 使用
using MyTypes = TypeList<int, double, std::string>;
static_assert(Length<MyTypes>::value == 3);
using Second = TypeAt<1, MyTypes>::type;  // double

16.4 编译期条件与分支

cpp 复制代码
// 编译期 if-else(用模板特化实现)
template<bool Condition, typename Then, typename Else>
struct If {
    using type = Then;
};

template<typename Then, typename Else>
struct If<false, Then, Else> {
    using type = Else;
};

// 等价于 std::conditional_t
using Result = If<(sizeof(int) > 4), int64_t, int32_t>::type;

16.5 检测是否存在成员(不用 void_t 的方式)

cpp 复制代码
// 经典 SFINAE 检测模式
template<typename T>
class HasSerialize {
    template<typename U>
    static auto test(int) -> decltype(std::declval<U>().serialize(), std::true_type{});

    template<typename U>
    static std::false_type test(...);

public:
    static constexpr bool value = decltype(test<T>(0))::value;
};

原理

  1. 编译器尝试 test<T>(int) 版本,如果 Tserialize() 方法,decltype(...) 合法,返回 true_type
  2. 如果 T 没有 serialize() 方法,decltype(...) 失败,SFINAE 排除这个重载。
  3. 编译器退而选择 test<T>(...) 版本(省略号参数匹配优先级最低),返回 false_type

16.6 服务器应用:编译期消息 ID 映射

cpp 复制代码
// 编译期关联消息类型和 ID
template<typename MsgType>
struct MsgTraits;

// 为每个消息类型特化
struct LoginReq { std::string username; };
template<>
struct MsgTraits<LoginReq> {
    static constexpr uint16_t ID = 1001;
    static constexpr const char* Name = "LoginReq";
};

struct MoveReq { float x, y, z; };
template<>
struct MsgTraits<MoveReq> {
    static constexpr uint16_t ID = 2001;
    static constexpr const char* Name = "MoveReq";
};

// 使用
constexpr uint16_t loginMsgId = MsgTraits<LoginReq>::ID;  // 1001,编译期确定

16.7 实践建议

模板元编程非常强大但也非常难读。实际工作中的建议:

  1. 优先使用 constexpr 函数做编译期计算,而不是模板递归。
  2. **优先使用 if constexpr**做编译期分支,而不是模板特化。
  3. 优先使用标准库的 type_traits,而不是自己从头写。
  4. 只在框架层面使用复杂的 TMP(如 ECS 框架、序列化框架、消息系统)。
  5. 业务逻辑代码中避免使用复杂的 TMP。

17. C++20 Concepts(概念)

17.1 是什么

Concepts 是 C++20 引入的特性,是 SFINAE 的人类友好替代品 。它允许你用清晰的语法来约束模板参数必须满足什么条件。

Concepts 本质上是对模板参数的约束。在 C++20 之前,模板可以接受任何类型,如果传入不合适的类型,编译器会给出冗长且难以理解的错误信息。Concepts 让你可以明确地声明"这个模板参数必须满足什么条件"。

17.2 基本语法

cpp 复制代码
#include <concepts>

// 简单定义一个 Concept(基于type trait)
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;
// 这里定义了一个叫 Numeric 的 concept,它要求类型 T 必须是算术类型(整数或浮点数)。std::is_arithmetic_v<T> 是一个编译期布尔值,为 true 时约束满足。

// 使用 Concept 约束模板参数
template<Numeric T>
T Add(T a, T b) {
    return a + b;
}

Add(3, 4);      // OK
Add(3.14, 2.0); // OK
// Add("a", "b"); // 编译错误,清晰的错误信息:string 不满足 Numeric

17.3 requires语法 和 定义 Concept 的方式

在此之前,先讲讲Concepts体系的核心语法requires,requires有两种不同的用法,本质都不一样:

  1. requires子句(requires clause) 出现在模板声明或函数签名中,用来施加约束:

    cpp 复制代码
    template<typename T>
    T add(T a, T b) requires std::integral<T> {  // 这是 requires 子句
        return a + b;
    }
    // 它的作用是说:"只有满足这个条件,这个模板才参与重载决议。"
  2. requires表达式(requires expression) 它是一个编译期布尔表达式,用来检测一组语法要求是否成立:

    cpp 复制代码
    requires(T a, T b) {
        a + b;           // 检查这些表达式是否合法
        a - b;
    }
    // 这个表达式本身的值是 true 或 false。
    
    // 两者可以嵌套出现,就形成了著名的 requires requires:
    template<typename T>
    T add(T a, T b) requires requires(T x, T y) { x + y; } {
        return a + b;
    }
    // 第一个 requires 是子句("我要求......"),第二个 requires 开始一个表达式("检测以下操作是否合法")。

requires表达式的完整语法:

cpp 复制代码
requires (参数列表) {
    要求1;
    要求2;
    ...
}
// 参数列表中的参数不会真正创建对象,只是为了在大括号里引用类型的"假想变量"。

// 大括号里可以写四种要求:
// 1. 简单要求(Simple Requirement)
// 检查某个表达式是否能通过编译,不关心返回类型:
template<typename T>
concept Addable = requires(T a, T b) {
    a + b;   // 只要 a + b 能编译通过就行
};

template<typename T>
concept HasToString = requires(T a) {
    a.toString();        // 必须有 toString() 方法
    a.size();            // 必须有 size() 方法
    std::cout << a;      // 必须支持 << 运算符
};
// 注意: 每一行末尾的分号是必须的,每行都是一个独立的要求。

// 2. 类型要求(Type Requirement)
// 检查某个类型是否存在,用 typename 关键字:
template<typename T>
concept HasValueType = requires {
    typename T::value_type;      // T 必须有 value_type 成员类型
    typename T::iterator;        // T 必须有 iterator 成员类型
    typename std::vector<T>;     // vector<T> 必须能实例化
};
// 这在检查容器类型时特别有用:
template<typename C>
concept Container = requires(C c) {
    typename C::value_type;
    typename C::size_type;
    typename C::iterator;
    c.begin();
    c.end();
    c.size();
};

// 3. 复合要求(Compound Requirement)
// 这是最强大的形式。不仅检查表达式能否编译,还能约束返回值类型和 noexcept 属性:
{ 表达式 } noexcept -> 类型约束;
// 三个部分都可以按需选用:
template<typename T>
concept MyType = requires(T a, T b) {
    // 只检查能否编译(等价于简单要求)
    { a + b };

    // 检查能否编译 + 返回类型约束
    { a + b } -> std::same_as<T>;

    // 检查能否编译 + 不抛异常
    { a + b } noexcept;

    // 全部检查
    { a.swap(b) } noexcept -> std::same_as<void>;
};
// 箭头 -> 后面跟的是一个 concept,编译器会自动把表达式的返回类型作为第一个参数传入。所以:
{ a + b } -> std::same_as<int>;
// 等价于检查:std::same_as<decltype(a + b), int>
{ a.size() } -> std::convertible_to<std::size_t>;
// 等价于检查:std::convertible_to<decltype(a.size()), std::size_t>

// 4. 嵌套要求(Nested Requirement)
// 在 requires 表达式内部再写一个 requires 子句,用来做编译期布尔判断:
template<typename T>
concept SignedNumeric = requires(T a) {
    a + a;
    a * a;
    requires std::is_signed_v<T>;      // 嵌套要求:必须是有符号类型
    requires sizeof(T) >= 4;            // 嵌套要求:至少 4 字节
};
// 嵌套要求以 requires 开头,后面跟一个编译期常量布尔表达式。注意和"简单要求"的区别:
requires(T a) {
    sizeof(T) >= 4;           // 简单要求:只检查这个表达式能否编译(永远为 true)
    requires sizeof(T) >= 4;  // 嵌套要求:检查这个值是否为 true
};
// 前者只问"这句话语法对不对"------当然对,比较运算永远合法。后者问"这个条件是不是真的"------只有 T 确实 ≥ 4 字节才满足。这个区别很容易出错。


// 把四种要求融合在一起:
template<typename T>
concept Serializable = requires(T obj, Buffer buf) {
    // 类型要求:必须有这些成员类型
    typename T::id_type;

    // 简单要求:必须有这些方法
    obj.serialize(buf);
    obj.deserialize(buf);

    // 复合要求:serialize 返回值必须是 void
    { obj.serialize(buf) } -> std::same_as<void>;

    // 复合要求:getId 必须不抛异常,且返回可转换为 uint32_t 的类型
    { obj.getId() } noexcept -> std::convertible_to<uint32_t>;

    // 嵌套要求:对象大小不能超过 1KB
    requires sizeof(T) <= 1024;

    // 嵌套要求:必须可以默认构造
    requires std::default_initializable<T>;
};

requires表达式可以单独使用:它本身就是一个 bool 值,所以可以用在任何需要编译期布尔的地方:

cpp 复制代码
// 用在 if constexpr 中
template<typename T>
void process(T val) {
    if constexpr (requires { val.toString(); }) {
        std::cout << val.toString();
    } else {
        std::cout << val;
    }
}

// 用在 static_assert 中
static_assert(requires(int a) { a + a; });  // 通过

// 用在变量中
constexpr bool can_add = requires(int a, int b) { a + b; };  // true

// 这比传统的 SFINAE 检测手法(写一大堆辅助模板)简洁太多了。

记住核心原则:简单要求只检查"能不能编译",要检查"值是不是 true"必须用嵌套 requires

接下来是定义 Concept 的方式:

cpp 复制代码
// 方式 1:基于 type_traits
template<typename T>
concept Integral = std::is_integral_v<T>;

// 方式 2:基于表达式合法性(requires 表达式)
template<typename T>
concept Serializable = requires(T obj, Buffer& buf) {
    { obj.serialize(buf) } -> std::same_as<void>;  // 必须有 serialize 方法
    { obj.deserialize(buf) } -> std::same_as<void>; // 必须有 deserialize 方法
    { T::MSG_ID } -> std::convertible_to<uint16_t>;  // 必须有 MSG_ID 常量
};
// 这是更强大的写法。requires 表达式里列出了类型 T 必须满足的所有条件:
// { obj.serialize(buf) } -> std::same_as<void>:表示对象必须有 serialize 方法,且返回值为 void
// { T::MSG_ID }:表示类型必须有一个叫 MSG_ID 的静态成员,且能转换为 uint16_t
// 花括号 {} 里写的是一个表达式,箭头 -> 后面是对该表达式返回值类型的约束。

// 方式 3:组合多个 Concept
template<typename T>
concept GameMessage = Serializable<T> && std::is_default_constructible_v<T>;
// 用 &&(逻辑与)把多个约束组合在一起。类型必须既满足 Serializable,又能默认构造。也可以用 ||(逻辑或)。

// 方式 4:复杂的 requires 子句
template<typename T>
concept Hashable = requires(T a) {
    { std::hash<T>{}(a) } -> std::convertible_to<size_t>;
};
// 用 &&(逻辑与)把多个约束组合在一起。类型必须既满足 Serializable,又能默认构造。也可以用 ||(逻辑或)。

template<typename Container>
concept Iterable = requires(Container c) {
    c.begin();
    c.end();
    { c.size() } -> std::convertible_to<size_t>;
};

17.4 使用 Concept 的四种语法

定义好 Concept 后,有多种方式使用它来约束模板:

cpp 复制代码
// 定义concept
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

// 语法 1:放在模板参数中(最推荐)
template<Numeric T>
T Add(T a, T b) { return a + b; }
// 把 typename 换成你的 concept 名字,最简洁。

// 语法 2:requires 子句
template<typename T>
requires Numeric<T>
T Subtract(T a, T b) { return a - b; }

// 语法 3:尾置 requires
template<typename T>
T Multiply(T a, T b) requires Numeric<T> { return a * b; }

// 语法 4:简写(auto + concept)
Numeric auto Divide(Numeric auto a, Numeric auto b) { return a / b; }
// 最简洁的写法,连 template<> 声明都省掉了。Numeric auto 表示"这个参数是满足 Numeric 的任意类型"。

17.5 Concepts vs SFINAE 对比

cpp 复制代码
// SFINAE 方式(C++11/14/17):晦涩
template<typename T,
         std::enable_if_t<std::is_integral_v<T>, int> = 0>
void Process(T value) { /* 整数处理 */ }

template<typename T,
         std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void Process(T value) { /* 浮点处理 */ }

// Concepts 方式(C++20):清晰
template<std::integral T>
void Process(T value) { /* 整数处理 */ }

template<std::floating_point T>
void Process(T value) { /* 浮点处理 */ }

// 对比非常明显------Concepts 写法更短、更直观、更易读。而且当类型不匹配时,Concepts 给出的错误信息比 SFINAE 清晰得多。

当你传入不匹配的类型时,Concepts 给出的错误信息比 SFINAE 清晰得多。

17.6 标准库中的 Concepts

C++20 <concepts> 头文件提供了一组常用 Concepts:

Concept 含义
std::integral 整数类型
std::floating_point 浮点类型
std::signed_integral 有符号整数
std::unsigned_integral 无符号整数
std::same_as<T, U> T 和 U 是相同类型
std::derived_from<D, B> D 继承自 B
std::convertible_to<From, To> From 可以转换为 To
std::default_initializable 可以默认构造
std::copyable 可以拷贝
std::movable 可以移动
std::invocable<F, Args...> F 可以用 Args 调用

17.7 游戏服务器应用

cpp 复制代码
// 定义游戏消息的 Concept
template<typename T>
concept GameMessage = requires {
    { T::MSG_ID } -> std::convertible_to<uint16_t>;
    requires std::is_default_constructible_v<T>;
};

// 消息注册函数:只接受满足 GameMessage 约束的类型
template<GameMessage MsgType>
void RegisterHandler(std::function<void(Connection*, const MsgType&)> handler) {
    // 编译期保证 MsgType 有 MSG_ID
    handlers_[MsgType::MSG_ID] = /* ... */;
}

// 如果传入不满足条件的类型,错误信息清晰
struct BadType {};
// RegisterHandler<BadType>(...);
// 错误信息:BadType does not satisfy GameMessage


18. 常见陷阱与调试技巧

模板代码的调试难度远高于普通代码。本节总结实际开发中最常踩的坑和应对方法。

18.1 依赖名称与 typename 消歧义

当一个名称依赖于模板参数 时,编译器不知道它是类型还是值。默认情况下编译器会认为它是值,如果实际上是类型,你需要显式加 typename

cpp 复制代码
template<typename T>
void foo() {
    // T::value_type 依赖于模板参数 T
    // 编译器不知道 value_type 是类型还是静态成员变量
    // 默认假设是值 → 导致编译错误

    // ❌ 编译错误:编译器把 T::value_type 当作值
    T::value_type x;

    // ✅ 加 typename 告诉编译器:这是一个类型
    typename T::value_type x;

    // ❌ 常见错误场景:迭代器
    T::iterator it = container.begin();

    // ✅ 正确写法
    typename T::iterator it = container.begin();
}

// C++20 中,在某些上下文(如 using 声明、基类列表)中 typename 可以省略,
// 但在变量声明中仍然需要。建议养成习惯:只要用了依赖于模板参数的嵌套类型,就加 typename。

18.2 依赖名称与 template 消歧义

类似的问题也出现在依赖于模板参数的成员模板调用上:

cpp 复制代码
template<typename T>
void bar(T& obj) {
    // obj.get<int>() 中的 < 会被编译器解析为"小于号"而不是模板参数列表
    // ❌ 编译错误
    auto val = obj.get<int>();

    // ✅ 在成员函数名前加 template 关键字
    auto val = obj.template get<int>();
}

// 实际场景:
template<typename Tuple>
void processTuple(Tuple& t) {
    // ❌ std::get<0>(t) 在这里没问题(std::get 不依赖于 Tuple)
    auto first = std::get<0>(t);  // OK

    // 但如果是依赖于模板参数的成员模板:
    // auto x = t.template get<0>();  // 需要加 template
}

规则总结:当 .->:: 后面跟的名称依赖于模板参数 ,并且后面紧跟 < ,就需要加 template 关键字。

18.3 两阶段名称查找(Two-Phase Lookup)

C++ 模板编译分两个阶段:

  1. 定义阶段:模板被解析时,检查不依赖模板参数的名称(非依赖名称)。
  2. 实例化阶段:模板被具体类型实例化时,检查依赖模板参数的名称(依赖名称)。

这会导致一个常见的坑------基类成员在派生类模板中"找不到"

cpp 复制代码
template<typename T>
class Base {
public:
    void baseMethod() {}
    int baseValue = 42;
};

template<typename T>
class Derived : public Base<T> {
public:
    void foo() {
        // ❌ 编译错误!编译器在定义阶段找不到 baseMethod
        // 因为 Base<T> 依赖于 T,编译器在第一阶段不会去 Base<T> 里查找
        baseMethod();

        // ✅ 三种修复方式:

        // 方式 1:用 this->(推荐,最清晰)
        this->baseMethod();

        // 方式 2:用基类限定名
        Base<T>::baseMethod();

        // 方式 3:用 using 声明引入
        // 放在类定义开头
    }
};

// 如果选择方式 3,在类的开头写:
template<typename T>
class Derived : public Base<T> {
    using Base<T>::baseMethod;  // 引入基类名称
    using Base<T>::baseValue;
public:
    void foo() {
        baseMethod();  // 现在可以直接用了
    }
};

18.4 CTAD------类模板参数推导(C++17)

C++17 引入了类模板参数推导(Class Template Argument Deduction,CTAD),允许从构造函数参数推导模板参数类型,省略尖括号:

cpp 复制代码
// C++17 之前
std::pair<int, double> p1(1, 3.14);
std::vector<int> v1 = {1, 2, 3};
std::tuple<int, double, std::string> t1(1, 3.14, "hello");

// C++17 CTAD:自动推导
std::pair p2(1, 3.14);                  // pair<int, double>
std::vector v2 = {1, 2, 3};             // vector<int>
std::tuple t2(1, 3.14, "hello");        // tuple<int, double, const char*>

注意事项 :CTAD 有时候推导的类型不是你想要的。比如上面 "hello" 被推导为 const char* 而不是 std::string。如果需要精确控制类型,还是显式写出模板参数。

自定义类也可以支持 CTAD:

cpp 复制代码
template<typename T>
class GamePool {
    std::vector<T> pool_;
public:
    GamePool(std::initializer_list<T> init) : pool_(init) {}
};

// 编译器根据构造函数自动推导
GamePool pool = {1, 2, 3, 4, 5};  // GamePool<int>

// 也可以写自定义推导指引(Deduction Guide)
template<typename Iter>
GamePool(Iter, Iter) -> GamePool<typename std::iterator_traits<Iter>::value_type>;

std::vector<float> src = {1.0f, 2.0f};
GamePool pool2(src.begin(), src.end());  // GamePool<float>

18.5 模板与 ADL(参数依赖查找)

ADL(Argument-Dependent Lookup,又叫 Koenig Lookup)是指:调用函数时,编译器除了在当前作用域查找,还会在参数类型所在的命名空间中查找。这在模板中有时会引发意外行为:

cpp 复制代码
namespace game {
    struct Player { int id; };

    // 这个 serialize 函数在 game 命名空间中
    void serialize(const Player& p) {
        std::cout << "game::serialize(Player)" << std::endl;
    }
}

template<typename T>
void process(const T& obj) {
    serialize(obj);  // ADL:如果 T 是 game::Player,会找到 game::serialize
}

// 调用
game::Player p{1};
process(p);  // 通过 ADL 找到 game::serialize ------ 有时候这正是你想要的

// 但如果你在全局也定义了一个 serialize,就可能产生歧义

ADL 在 STL 中被广泛利用(比如 swapbeginend),但在自己写模板库时需要注意:

cpp 复制代码
// 最佳实践:自定义 swap 放在类型自己的命名空间中
namespace game {
    struct Player { /* ... */ };
    void swap(Player& a, Player& b) noexcept {
        // 高效交换
    }
}

// 模板中调用 swap 的正确方式
template<typename T>
void doSomething(T& a, T& b) {
    using std::swap;   // 先引入 std::swap 作为后备
    swap(a, b);        // ADL 会优先找到类型自己命名空间的 swap
}

18.6 模板调试实用技巧汇总

  1. static_assert 是你最好的朋友 :在模板入口处用 static_assert 检查类型约束,给出清晰的错误信息,避免深层的编译错误瀑布。
cpp 复制代码
template<typename T>
class Buffer {
    static_assert(!std::is_pointer_v<T>,
        "Buffer<T> 不接受指针类型,请使用 Buffer<PointedType> 并自行管理生命周期");
    static_assert(std::is_trivially_copyable_v<T>,
        "Buffer<T> 要求 T 可以安全 memcpy,否则请使用 SafeBuffer<T>");
    // ...
};
  1. 打印模板推导结果 :编译期想看 T 被推导成什么类型时,故意触发一个编译错误:
cpp 复制代码
// 辅助工具:不定义这个模板,让编译器报错时显示 T 的类型
template<typename T>
struct TypeDebugger;  // 故意不定义

template<typename T>
void myFunc(T&& arg) {
    TypeDebugger<T> debug;          // 编译器报错信息会显示 T 的实际类型
    TypeDebugger<decltype(arg)> d2; // 显示 arg 的实际类型
}
  1. 从第一个错误开始读 :模板错误往往是瀑布式的,后面的错误大多是连锁反应。重点看第一个错误中的 required from here(触发实例化的位置)和 candidate template ignored(编译器尝试了什么但失败了)。
  2. 分步实例化 :如果模板嵌套很深,先用具体类型手动 typedef 中间结果,逐步缩小问题范围:
cpp 复制代码
// 如果这行报错且难以理解
auto result = Transform<Filter<Map<MyList, Func1>, Pred>, Func2>{};

// 分步展开,逐步定位
using Step1 = Map<MyList, Func1>;
using Step2 = Filter<Step1, Pred>;
using Step3 = Transform<Step2, Func2>;  // 哪步报错一目了然
  1. C++20 Concepts 大幅改善错误信息:如果项目允许使用 C++20,给模板参数加上 Concept 约束后,编译器的错误信息会从"几百行的模板实例化瀑布"变成"类型 X 不满足约束 Y"这种一行就能理解的提示。这是升级到 C++20 的最大动力之一。

19. 综合实战:游戏服务器消息分发系统

这个实战项目把前面学到的所有技术串联起来,实现一个类型安全、编译期注册、零样板代码的消息分发系统。

19.1 设计目标

复制代码
1. 用 Protobuf-like 的消息结构(带 MSG_ID)
2. 消息 ID → Handler 的映射在编译期自动完成
3. 类型安全:Handler 接收的是具体消息类型,不是 void*
4. 支持批量注册
5. 对外接口简洁

19.2 完整实现

cpp 复制代码
#include <iostream>
#include <functional>
#include <unordered_map>
#include <memory>
#include <cassert>
#include <cstdint>
#include <string>

// ============================================================
// Part 1: 基础设施
// ============================================================

// 模拟一个网络缓冲区
class Buffer {
    std::string data_;
public:
    const std::string& data() const { return data_; }
    void setData(const std::string& d) { data_ = d; }
};

// 模拟一个网络连接
class Connection {
    uint64_t id_;
public:
    Connection(uint64_t id) : id_(id) {}
    uint64_t getId() const { return id_; }
};

// ============================================================
// Part 2: 消息特征(使用 CRTP + 模板元编程)
// ============================================================

// Concept:定义什么是合法的消息类型
template<typename T>
concept GameMessage = requires {
    { T::MSG_ID } -> std::convertible_to<uint16_t>;
    { T::MSG_NAME } -> std::convertible_to<const char*>;
    requires std::is_default_constructible_v<T>;
};

// 消息定义示例
struct LoginReq {
    static constexpr uint16_t MSG_ID = 1001;
    static constexpr const char* MSG_NAME = "LoginReq";
    std::string username;
    std::string password;
};

struct LoginRes {
    static constexpr uint16_t MSG_ID = 1002;
    static constexpr const char* MSG_NAME = "LoginRes";
    bool success;
    std::string token;
};

struct MoveReq {
    static constexpr uint16_t MSG_ID = 2001;
    static constexpr const char* MSG_NAME = "MoveReq";
    float x, y, z;
};

struct ChatReq {
    static constexpr uint16_t MSG_ID = 3001;
    static constexpr const char* MSG_NAME = "ChatReq";
    std::string channel;
    std::string content;
};

// ============================================================
// Part 3: 类型擦除的 Handler 存储
// ============================================================

// 非模板基类(类型擦除的关键)
class IMessageHandler {
public:
    virtual void handle(Connection* conn, const void* msgData) = 0;
    virtual const char* getMsgName() const = 0;
    virtual ~IMessageHandler() = default;
};

// 模板派生类:持有具体类型的处理函数
template<GameMessage MsgType>
class TypedMessageHandler : public IMessageHandler {
    using HandlerFunc = std::function<void(Connection*, const MsgType&)>;
    HandlerFunc func_;

public:
    TypedMessageHandler(HandlerFunc f) : func_(std::move(f)) {}

    void handle(Connection* conn, const void* msgData) override {
        func_(conn, *static_cast<const MsgType*>(msgData));
    }

    const char* getMsgName() const override {
        return MsgType::MSG_NAME;
    }
};

// ============================================================
// Part 4: 消息分发器
// ============================================================

class MessageDispatcher {
    std::unordered_map<uint16_t, std::unique_ptr<IMessageHandler>> handlers_;

public:
    // 注册单个 Handler(Concept 约束保证类型安全)
    template<GameMessage MsgType>
    void registerHandler(std::function<void(Connection*, const MsgType&)> handler) {
        auto typedHandler = std::make_unique<TypedMessageHandler<MsgType>>(std::move(handler));
        std::cout << "[Register] MsgID=" << MsgType::MSG_ID
                  << ", Name=" << MsgType::MSG_NAME << std::endl;
        handlers_[MsgType::MSG_ID] = std::move(typedHandler);
    }

    // 分发消息
    void dispatch(Connection* conn, uint16_t msgId, const void* msgData) {
        auto it = handlers_.find(msgId);
        if (it != handlers_.end()) {
            it->second->handle(conn, msgData);
        } else {
            std::cout << "[Warn] No handler for MsgID=" << msgId << std::endl;
        }
    }

    // 批量注册:用折叠表达式展开(变参模板 + fold expression)
    template<GameMessage... MsgTypes, typename... Handlers>
    void registerAll(std::pair<std::function<void(Connection*, const MsgTypes&)>,
                     Handlers>... /* 这个方式太复杂,用另一种 */) {}

    // 更实用的批量注册方式
    template<GameMessage MsgType, typename Handler>
    void reg(Handler&& handler) {
        registerHandler<MsgType>(
            std::function<void(Connection*, const MsgType&)>(
                std::forward<Handler>(handler)));
    }

    bool hasHandler(uint16_t msgId) const {
        return handlers_.find(msgId) != handlers_.end();
    }
};

// ============================================================
// Part 5: 使用示例
// ============================================================

// 模拟一个游戏服务器类
class GameServer {
    MessageDispatcher dispatcher_;

public:
    void init() {
        // 注册消息处理器
        dispatcher_.reg<LoginReq>([this](Connection* conn, const LoginReq& req) {
            onLoginReq(conn, req);
        });

        dispatcher_.reg<MoveReq>([this](Connection* conn, const MoveReq& req) {
            onMoveReq(conn, req);
        });

        dispatcher_.reg<ChatReq>([this](Connection* conn, const ChatReq& req) {
            onChatReq(conn, req);
        });
    }

    void onMessage(Connection* conn, uint16_t msgId, const void* data) {
        dispatcher_.dispatch(conn, msgId, data);
    }

private:
    void onLoginReq(Connection* conn, const LoginReq& req) {
        std::cout << "[Login] User=" << req.username
                  << " from conn " << conn->getId() << std::endl;
    }

    void onMoveReq(Connection* conn, const MoveReq& req) {
        std::cout << "[Move] (" << req.x << ", " << req.y << ", " << req.z
                  << ") from conn " << conn->getId() << std::endl;
    }

    void onChatReq(Connection* conn, const ChatReq& req) {
        std::cout << "[Chat] [" << req.channel << "] " << req.content
                  << " from conn " << conn->getId() << std::endl;
    }
};

// 测试
int main() {
    GameServer server;
    server.init();

    Connection conn(12345);

    LoginReq loginReq{"Alice", "password123"};
    server.onMessage(&conn, LoginReq::MSG_ID, &loginReq);

    MoveReq moveReq{100.0f, 200.0f, 0.0f};
    server.onMessage(&conn, MoveReq::MSG_ID, &moveReq);

    ChatReq chatReq{"world", "Hello everyone!"};
    server.onMessage(&conn, ChatReq::MSG_ID, &chatReq);

    // 未注册的消息
    server.onMessage(&conn, 9999, nullptr);

    return 0;
}

19.3 这个系统用到了哪些技术

技术 应用位置
函数模板 registerHandler<MsgType>()reg<MsgType>()
类模板 TypedMessageHandler<MsgType>
C++20 Concepts GameMessage 约束消息类型
类型擦除 IMessageHandler 基类 + TypedMessageHandler 派生类
模板元编程 MsgType::MSG_ID 编译期消息 ID
完美转发 std::forward<Handler>(handler)
智能指针 std::unique_ptr<IMessageHandler>

20. 总结与学习路径建议

20.1 知识体系全景图

复制代码
C++ 模板与泛型编程知识体系
│
├── 基础层(必须掌握)
│   ├── 函数模板 / 类模板
│   ├── 模板特化与偏特化
│   ├── 模板编译模型(头文件规则、extern template)
│   ├── 非类型模板参数
│   └── 万能引用与完美转发(std::forward / std::move)
│
├── 类型系统层(必须掌握)
│   ├── type_traits(类型查询与变换)
│   ├── SFINAE / enable_if(编译期条件重载)
│   ├── Tag Dispatch(标签分发)
│   └── std::void_t / declval(表达式检测)
│
├── 现代 C++ 层(强烈推荐)
│   ├── 变参模板(参数包展开)
│   ├── 折叠表达式(C++17)
│   ├── if constexpr(C++17)
│   ├── CTAD 类模板参数推导(C++17)
│   └── Concepts(C++20)
│
├── 设计模式层(重要)
│   ├── CRTP(静态多态)
│   ├── Mixin 模式(功能组合)
│   ├── 类型擦除(统一存储)
│   ├── 模板模板参数与策略模式
│   └── 表达式模板(Expression Templates)
│
├── 高级层(了解即可)
│   ├── 模板元编程 TMP
│   ├── 编译期类型列表
│   └── std::tuple + index_sequence
│
└── 工程实践层(持续积累)
    ├── 依赖名称消歧义(typename / template)
    ├── 两阶段名称查找(Two-Phase Lookup)
    ├── ADL(参数依赖查找)
    └── 模板调试技巧(static_assert、TypeDebugger)

20.2 技术选型决策指南

面对具体问题时,应该选哪种模板技术?下表按常见场景给出推荐方案和权衡分析。

编译期分支与条件选择
场景 推荐技术 优势 劣势 / 注意事项
同一个函数体内,根据类型走不同逻辑 if constexpr(C++17) 最简洁直观,一个函数搞定 不能改变函数签名(返回类型、参数列表必须统一)
不同类型需要完全不同的函数签名或返回类型 SFINAE / enable_if(C++17 前);Concepts(C++20) 可以有不同返回类型、不同参数列表 SFINAE 语法晦涩;Concepts 需要 C++20
为特定类型提供完全不同的实现 模板全特化 / 偏特化 语义明确,每个特化版本独立完整 函数模板不支持偏特化;特化版本多了维护成本高
根据类型特征(如 is_trivially_copyable)选择算法 Tag Dispatch 可读性好,每个分支是独立函数,易扩展新标签 需要定义额外的标签类型;分支数量少时显得过重
约束模板参数必须满足某些条件 Concepts(C++20 优先);static_assert(通用);SFINAE(C++17 前) Concepts 错误信息最清晰;static_assert 最简单 static_assert 不参与重载决议,只能报错不能选择分支
类型检测与萃取
场景 推荐技术 优势 劣势 / 注意事项
查询类型基本属性(是否整数、是否可拷贝等) 标准库 <type_traits> 现成可用,经过充分测试 只覆盖标准属性,自定义属性需要自己写
检测类型是否有某个成员函数 / 成员变量 std::void_t + 偏特化(C++17);requires 表达式(C++20) void_t 可检测任意表达式合法性;requires 写法更简洁 void_t 模式初学者不易理解
编译期类型变换(去 const、去引用等) 标准库 remove_const_tdecay_t 直接使用,无需自己实现 组合变换时嵌套较深,可读性下降
自定义类型分类(如"是否是游戏消息类型") 自定义 type_trait(主模板 + 特化),或 Concept 可复用、可组合 手动特化方式需要为每个类型单独写特化
多态与代码复用
场景 推荐技术 优势 劣势 / 注意事项
需要多态且不同类型要放在同一容器中 虚函数(运行时多态) 不同派生类可统一用基类指针管理 vtable 间接调用开销 + 可能的 cache miss
需要多态但追求极致性能(每帧调用百万次) CRTP(静态多态) 编译期绑定,零运行时开销,可内联 不同 Base<Derived> 是不同类型,不能放同一容器;不能运行时动态切换
需要 CRTP 的性能又需要统一存储 CRTP + 类型擦除(虚基类包一层) 兼顾编译期优化和运行时灵活性 最外层仍有虚函数开销;设计复杂度较高
给类链式叠加功能(日志 → 加密 → 限频) Mixin 模式 功能可自由组合;每层可拦截和增强基类行为 继承链较深时调试困难;编译错误信息可能很长
编译期注入可替换的策略(分配器、序列化器) 模板模板参数 + 策略模式 零运行时开销;策略间互不依赖,正交组合 策略只能在编译期确定,不能运行时切换
不同模板实例类型需要统一存储和管理 类型擦除(非模板基类 + 模板派生类) 可以把 Handler<A>Handler<B> 放在同一个 map 需要堆分配(SBO 可缓解);虚函数间接调用开销
参数处理与转发
场景 推荐技术 优势 劣势 / 注意事项
转发参数给下层函数,保持左值 / 右值属性 万能引用 T&& + std::forward<T> 完美保持值类别,零额外拷贝 同一对象不能多次 forward;概念较难理解
处理任意数量、任意类型的参数 变参模板 Args... + 折叠表达式(C++17) 类型安全,编译期展开,支持任意参数组合 C++11 需用递归展开,写法繁琐;错误信息可能很长
工厂函数:构造任意类型的对象 变参模板 + 完美转发 参数直接转发给构造函数,避免中间拷贝 需要同时理解万能引用、参数包展开、forward
编译期计算
场景 推荐技术 优势 劣势 / 注意事项
编译期计算数值(阶乘、哈希、查找表) constexpr 函数(优先);模板递归(TMP) constexpr 函数写法接近普通代码,易读易维护 模板递归可读性极差;只适用于输入在编译期已知的场景
编译期操作类型列表(遍历、过滤、变换) std::tuple + std::index_sequence + 折叠表达式 标准库支持好,配合 std::apply 很方便 嵌套模板多时编译时间显著增加
编译期关联类型与值(消息类型 → 消息 ID) 模板特化(MsgTraits<T>)或 Concept + constexpr 静态成员 编译期确定映射关系,类型安全 每新增一个类型都需要写一个特化或满足 Concept 约束
速查:相似技术如何选择
对比 选 A 的场景 选 B 的场景
if constexpr(A) vs SFINAE(B) 分支逻辑在一个函数体内、不需要改变签名 需要不同的返回类型或参数列表、需要参与重载决议
SFINAE(A) vs Concepts(B) 项目限制在 C++17 及以下 可以用 C++20,追求可读性和清晰的错误信息
CRTP(A) vs 虚函数(B) 类型在编译期确定、性能敏感、不需要运行时切换 类型在运行时才确定、需要统一容器存储、需要动态分发
Mixin(A) vs 策略模式(B) 需要层层拦截和增强行为(装饰器链) 策略之间互相独立、只需要组合而不需要链式调用
Tag Dispatch(A) vs if constexpr(B) 分支较多且需要独立函数(可单独测试)、需要可扩展性 分支较少、逻辑简单、不想引入额外标签类型
类型擦除(A) vs 模板参数(B) 不同类型需要在运行时统一管理(存入同一容器) 类型在编译期确定、追求零开销
std::function(A) vs 函数指针(B) 需要捕获上下文的 lambda、代码简洁优先 性能敏感的热路径(每帧百万次调用)、不需要捕获
constexpr 函数(A) vs TMP 模板递归(B) 任何场景(优先选 A) 需要操作类型(而非数值)、C++11 且无法用 constexpr
模板特化(A) vs Concept 约束(B) 需要为特定类型提供完全不同的实现 只需要约束类型满足某些条件,不需要改变实现

20.3 学习路径建议

模板与泛型编程的知识点繁多且环环相扣,以下是一条推荐的学习路线,按阶段递进:

第一阶段:打好地基(1~2 周)

目标是能写出基本的模板代码、理解编译模型,不再踩"链接失败"的坑。

重点内容:函数模板、类模板、非类型模板参数、模板的默认参数、模板代码为什么必须放在头文件中、显式实例化与 extern template

练习建议:自己实现一个泛型 Stack<T>;把一个重复了多种类型的工具函数改写成模板;故意把模板定义放在 .cpp 中,观察链接错误并修复。

第二阶段:掌握类型系统工具(2~3 周)

目标是理解编译器如何在编译期做类型判断和条件选择,这是后续所有高级技术的基石。

重点内容:type_traitsis_integralis_trivially_copyableremove_constdecay 等)、模板特化与偏特化、SFINAE 的原理、std::enable_if 的三种用法、std::void_t 检测表达式合法性、Tag Dispatch。

练习建议:写一个 WriteToBuffer 函数,对 POD 类型用 memcpy,对非 POD 类型调用 .Serialize() 方法;自己实现 has_begin<T> 检测一个类型有没有 begin() 方法;用 Tag Dispatch 实现一个根据迭代器类别选择不同算法的 MyAdvance 函数。

第三阶段:拥抱现代 C++(2~3 周)

目标是用 C++17/20 的新特性大幅简化模板代码,替代冗长的 SFINAE。

重点内容:万能引用与完美转发、变参模板与参数包展开(递归展开、逗号展开)、折叠表达式(四种形式)、if constexpr 编译期分支、C++20 Concepts(requires 子句与表达式、四种使用语法)。

练习建议:用折叠表达式实现 SumPrintAllTrue;把之前用 SFINAE 写的代码改写成 if constexpr 版本和 Concepts 版本,对比可读性;实现一个泛型事件发射器 EventEmitter

第四阶段:设计模式实战(2~4 周)

目标是学会在真实项目中选择和组合模板设计模式。

重点内容:CRTP(静态多态、实例计数器、静态接口强制)、Mixin 模式(功能链式组合)、类型擦除(IHandler + TypedHandler 模式、手写 SimpleFunction)、完美转发与工厂函数、模板模板参数与策略模式。

练习建议:用 CRTP 实现一个带独立计数的对象池 ObjectPool<T>;用 Mixin 模式给一个网络连接类组合上日志、加密、限频功能;手写一个简化版的 std::function,理解类型擦除的精髓。

第五阶段:高级主题与综合(按需)

这一阶段的内容更偏底层和框架层,日常业务开发中使用频率较低,但在写高性能框架时非常有价值。

重点内容:模板元编程(编译期计算、编译期类型列表)、std::tuple + std::index_sequencestd::apply、Expression Templates(了解即可)。

练习建议:实现本文第 19 节的"游戏服务器消息分发系统",把所有技术串联起来;在自己的项目中找一个合适的场景(如序列化框架、ECS 组件注册),尝试用模板技术重构。

通用学习建议:

  1. 每学一个技术点,都动手写代码验证,不要只看不写。模板的很多细节(比如 SFINAE 的匹配规则、折叠表达式的展开方向)只有自己写了才能真正理解。
  2. 善用 static_assert 做编译期检查,它是调试模板代码的利器。
  3. 善用编译器的错误信息。GCC 和 Clang 的模板错误信息虽然长,但其中 required from herecandidate template ignored 是关键线索。
  4. 优先选择更简单的方案:能用 if constexpr 就不用 SFINAE,能用 Concepts 就不用 enable_if,能用 constexpr 函数就不用模板递归。
  5. 模板是工具,不是目的。在业务逻辑中保持简单直接,只在框架和基础设施层面使用复杂的模板技术。过度使用模板会严重降低代码的可读性和可维护性。
相关推荐
minji...9 小时前
Linux 多线程(一)线程概念,轻量级进程,执行流,线程创建
java·开发语言·jvm
2401_892070989 小时前
【Linux C++ 日志系统实战】Logger 日志器完整实现:级别控制、宏封装、动态输出、自动崩溃退出
linux·c++·日志系统
cch89189 小时前
易语言 vs Go:初学者与专业开发之选
开发语言·后端·golang
B1acktion9 小时前
2.7.希尔排序——让插入排序先大步走,再小步收尾
c++·算法·排序算法
0xDevNull9 小时前
Java 17 新特性概览与实战教程
java·开发语言·后端
java1234_小锋9 小时前
Python高频面试题:python里面模块和包之间有什么区别?
开发语言·python
蓝天居士9 小时前
cpio命令详解(1)
linux·cpio
原来是猿9 小时前
Linux进程信号详解(一):信号快速认识
linux·c++·算法
minji...9 小时前
Linux 多线程(二)进程虚拟地址空间&&页表&&物理地址
linux·运维·服务器