Skip to content

模板元编程

模板

template, 编译期的语言, 一方面可以看成是 “ 以数据类型作为变量的函数 ”; 另一方面可以视作 “ 在编译期进行的代码生成 ( 代码渲染 ) ”

简单语法

C++
template<typename T>
function...

其中定义的函数本身是一个接收变量然后实现某些功能的函数, 比如 int AddFunc(int a, int b) 中的 ab, 另一方面, 使用 typename 定义的“范型”类型 T 也是一个变量, 提供一个具体例子

C++
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 等
  • 分支结构: 模板特化
  • 循环: 递归

先牛刀小试——

C++
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 值变量,如果为真则进入分支,假则进入其他匹配. 可读性会比较高, 同时也会使得报错信息更好看一点.

当然或许更想是一种约束, 约束只有符合条件的才可.

C++
// 判断是否是 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) {}

同时,可以做到在编译期实现类型自省的能力——判断类型是否正确 / 是否拥有某方法等.

C++
template <typename T>
concept HasGetReflectedClassName = requires(T t) {
	{ t.GetReflectedClassName() } -> std::convertible_to<std::string>;
};

比如上述代码就是在做一种测试——类型必须有GetReflectedClassName()方法并且可以通过类型转化成std::string

CRTP

静态多态

最基础的用法——静态多态, 可以规避 虚表和虚函数指针 跳转时候的开销,极致的压榨性能.

C++
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 中, 实现了 RenderTick 功能,同时没有虚函数的额外开销,因为一切不是在运行时寻址,而是在编译期就已经定好.

当然,在此之外,更好的一种处理方式就是用concepts去约束,确保一定包含某必须实现的方法 ( 鸭子类型 )

Mixin

Mixin 机制, 首先自己的理解是类似组合——“子类有什么能力”, 或者可以说是一种接口,但是已经给定实现了.

C++
// 功能模块 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, 在父类的方法中返回子类的指针

C++
// 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;
};

Released under the MIT License.