模板元编程
模板
template, 编译期的语言, 一方面可以看成是 “ 以数据类型作为变量的函数 ”; 另一方面可以视作 “ 在编译期进行的代码生成 ( 代码渲染 ) ”
简单语法
template<typename T>
function...其中定义的函数本身是一个接收变量然后实现某些功能的函数, 比如 int AddFunc(int a, int b) 中的 a 和 b, 另一方面, 使用 typename 定义的“范型”类型 T 也是一个变量, 提供一个具体例子
template<typename T>
T Add(T a, T b)
{
return a + b;
}
Add<int> (1, 2);
Add<float> (1, 2);
Add<SomethineElse> (A, B); // 其中需要重载了 + 运算符那么在编译期间,会被展开成 int,float 等所有在源码中使用过的类型, 比如 ——
Add<int> -> int __Add_int(int, int) ...Add<float> -> float __Add_float(float, float) ...
非常方便,但是如果只是当作一个范型方法, 便没有发挥出它的全部威力.
图灵完备性
- 变量: Type 等
- 分支结构: 模板特化
- 循环: 递归
先牛刀小试——
template <int N>
struct Factorial {
static const int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static const int value = 1;
};
int x = Factorial<5>::value; // 5!
// 实际上可以直接这样写
constexpr int Factorial(int n) { return (n <= 1) ? 1 : n * Factorial(n - 1); }
// 就会在编译器直接进行计算
int x = Factorial(5);在编译期发现 Facttorial<5> 的时候,会实例化 Factorial<5>, 然后发现其又实例化了 Factorial<5-1> 以此类推, 实现了递归的过程, 并且在 Factorial<0>的时候做了模板特化,相当于递归终止条件,得到运算结果.
也就是说,可以在编译期进行所有的运算,但是没有必要,这会导致生成的代码异常冗长而且难以理解 (
SFINAE
Substitution Failure Is Not An Error
在模板实例化时,编译器会尝试将模板参数替换为具体类型。如果替换过程中产生了毫无意义的、非法的语法结构(比如尝试获取 int::value),编译器不会报错停止编译,而是默默将这个模板从候选重载列表中剔除,继续寻找下一个合适的重载。
简单来说,就是不断的匹配,匹配到符合条件的重载就好.
在 C++20 之前,使用enable_if_t等语法,可读性比较低,在下面以20开始的 concepts 来进行说明 ——
实际上, concepts 语法是在维护了一个编辑期计算的 bool 值变量,如果为真则进入分支,假则进入其他匹配. 可读性会比较高, 同时也会使得报错信息更好看一点.
当然或许更想是一种约束, 约束只有符合条件的才可.
// 判断是否是 String 类型
template <typename T>
concept IsString = std::same_as<std::remove_cvref_t<T>, std::string>;
// 具体使用, 此处只有 std::string 类型 才可以进入该分支,
// 也就是进行了约束
// 所以里面不需要再进行任何 check 一定是 string 类型
template <typename T>
requires IsString<T>
void Foo(T str) {}
// 其他两种简写形式
template <IsString T>
void Foo(T str) {}
// 使用 auto 最简写法
void Foo(IsString auto str) {}同时,可以做到在编译期实现类型自省的能力——判断类型是否正确 / 是否拥有某方法等.
template <typename T>
concept HasGetReflectedClassName = requires(T t) {
{ t.GetReflectedClassName() } -> std::convertible_to<std::string>;
};比如上述代码就是在做一种测试——类型必须有GetReflectedClassName()方法并且可以通过类型转化成std::string类
CRTP
静态多态
最基础的用法——静态多态, 可以规避 虚表和虚函数指针 跳转时候的开销,极致的压榨性能.
template <typename T>
class ISystem {
public:
void Tick() { static_cast<T*>(this)->TickImpl(); }
};
class Render : public ISystem<Render> {
public:
void TickImpl() { std::cout << "On Render Tick" << std::endl; }
};
// 业务代码
template <typename T>
void Tick(T& system) {
system.Tick();
}在 Render 类实例化的时候, ISystem 里面的模板部分就被 “ 注入 ” 到 Render 中, 实现了 Render 的 Tick 功能,同时没有虚函数的额外开销,因为一切不是在运行时寻址,而是在编译期就已经定好.
当然,在此之外,更好的一种处理方式就是用concepts去约束,确保一定包含某必须实现的方法 ( 鸭子类型 )
Mixin
Mixin 机制, 首先自己的理解是类似组合——“子类有什么能力”, 或者可以说是一种接口,但是已经给定实现了.
// 功能模块 1:引用计数能力
template <typename Derived>
struct RefCountable {
void Retain() { ++count; }
void Release() { if (--count == 0) delete static_cast<Derived*>(this); }
private:
int count = 0;
};
// 功能模块 2:唯一 ID 能力
template <typename Derived>
struct Identifiable {
uint64_t GetID() const { return reinterpret_cast<uint64_t>(this); }
};
// Entity 组合了引用计数和身份识别的能力。
class GameEntity : public RefCountable<GameEntity>,
public Identifiable<GameEntity> {
public:
void Update() { /* ... */ }
};
int main() {
GameEntity* e = new GameEntity();
e->Retain(); // 获得了 RefCountable 的能力
auto id = e->GetID(); // 获得了 Identifiable 的能力
}Polymorphic Chaining
多态链式调用, 在子类调用父类链式方法返回*this的时候,如果在后面继续跟着子类特有的链式方法,就会导致错误——父类返回的类型是父类类型, 导致在继续链式查找的时候,无法查找子类的相关方法.
此时就需要使用 CRTP, 在父类的方法中返回子类的指针
// 1. 把基类变成模板,接受 Derived 作为参数
template <typename Derived>
class Widget {
public:
// 返回值类型写死为 Derived& (也就是具体的子类 Button&)
Derived& SetPos(int x, int y) {
this->x = x; this->y = y;
// 既然我是 Widget<Derived>,那我一定是被 Derived 继承的。
// 把 *this (基类部分) 强转为 派生类引用。
return static_cast<Derived&>(*this);
}
Derived& SetSize(int w, int h) {
this->w = w; this->h = h;
return static_cast<Derived&>(*this);
}
protected:
int x, y, w, h;
};
// 2. 派生类把自己传进去
class Button : public Widget<Button> {
public:
Button& SetText(const char* text) {
this->text = text;
// 这里本来就是 Button,直接返回 *this
return *this;
}
private:
const char* text;
};