在看代码时遇到这样一种写法,不是很理解,把模版类自己用作模版参数:

1
class MyStubService : public DbusServer<MyStubService> {};

咨询了 DeepSeek 后得知这是 C++ 中的 CRTP(Curiously Recurring Template Pattern,奇异递归模板模式)

CRTP 是一种借助模板实现编译时多态的技术,核心思想是:类模板以其派生类作为模板参数

CRTP 的优势

  1. 性能优势:避免了虚函数调用的运行时开销
  2. 编译时优化:编译器可以进行更好的内联和优化
  3. 类型安全:在编译时捕获类型错误
  4. 减少二进制大小:不需要虚函数表

CRTP 基本语法结构

1
2
3
4
5
6
7
8
template <typename Derived>
class Base {
    // 基类定义
};

class Derived : public Base<Derived> {  // 关键:派生类将自己作为模板参数传递给基类
    // 派生类定义
};

CRTP 主要应用场景

1. 静态多态(编译时多态)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
template <typename Derived>
class Animal {
public:
    void speak() {
        static_cast<Derived*>(this)->speakImpl();  // 编译时绑定
    }
    
    void run() {
        static_cast<Derived*>(this)->runImpl();
    }
};

class Dog : public Animal<Dog> {
public:
    void speakImpl() {
        std::cout << "Woof!" << std::endl;
    }
    
    void runImpl() {
        std::cout << "Running on four legs" << std::endl;
    }
};

class Cat : public Animal<Cat> {
public:
    void speakImpl() {
        std::cout << "Meow!" << std::endl;
    }
    
    void runImpl() {
        std::cout << "Running gracefully" << std::endl;
    }
};

// 使用
template <typename T>
void makeAnimalSpeak(Animal<T>& animal) {
    animal.speak();  // 编译时确定调用哪个实现
}

2. 对象计数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template <typename T>
class Counter {
private:
    static int count;
protected:
    Counter() { ++count; }
    Counter(const Counter&) { ++count; }
    ~Counter() { --count; }
public:
    static int getCount() { return count; }
};

// 静态成员初始化
template <typename T>
int Counter<T>::count = 0;

// 使用
class MyClass : public Counter<MyClass> {
    // 自动获得计数功能
};

class AnotherClass : public Counter<AnotherClass> {
    // 独立的计数
};

3. 链式调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
template <typename Derived>
class Chainable {
public:
    Derived& self() { return static_cast<Derived&>(*this); }
    
    Derived& setX(int x) {
        // 具体实现由派生类提供
        self().setXImpl(x);
        return self();  // 返回派生类引用,支持链式调用
    }
    
    Derived& setY(int y) {
        self().setYImpl(y);
        return self();
    }
};

class Point : public Chainable<Point> {
private:
    int x, y;
public:
    Point& setXImpl(int x_) { x = x_; return *this; }
    Point& setYImpl(int y_) { y = y_; return *this; }
    
    void print() const {
        std::cout << "Point(" << x << ", " << y << ")" << std::endl;
    }
};

// 使用:链式调用
Point p;
p.setX(10).setY(20).print();

4. 实现运算符重载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <typename Derived>
class Comparable {
public:
    bool operator==(const Derived& other) const {
        return static_cast<const Derived*>(this)->equals(other);
    }
    
    bool operator!=(const Derived& other) const {
        return !static_cast<const Derived*>(this)->equals(other);
    }
};

class Person : public Comparable<Person> {
private:
    std::string name;
    int age;
public:
    Person(const std::string& n, int a) : name(n), age(a) {}
    
    bool equals(const Person& other) const {
        return name == other.name && age == other.age;
    }
};

5. 静态接口检查(约束派生类的必备成员)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template <typename Derived>
class MustHaveFoo {
public:
    void callFoo() {
        static_assert(std::is_same_v<decltype(&Derived::foo), void(Derived::*)()>,
                      "Derived must define void foo() member");
        static_cast<Derived*>(this)->foo();
    }
};

class OK : public MustHaveFoo<OK> {
public:
    void foo() {}
};

也可用 C++20 Concepts 直接约束:

1
2
3
4
5
template <typename T>
concept HasFoo = requires(T t) { t.foo(); };

template <HasFoo Derived>
class MustHaveFoo2 { /* ... */ };

6. 空基类优化(EBO)

CRTP 基类常为空(仅编译期行为),作为空基类被继承时通常不占用额外空间,有助于零开销地叠加特性。

7. Barton–Nackman 技巧(基于 CRTP 的友元运算符)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template <typename Derived>
class Orderable {
public:
    friend bool operator<(const Derived& a, const Derived& b) {
        return a.less(b);
    }
};

class Node : public Orderable<Node> {
public:
    bool less(const Node& other) const { return key < other.key; }
private:
    int key{0};
};

CRTP 的局限性

  1. 编译时绑定:不能在运行时动态改变行为
  2. 模板复杂性:错误信息可能难以理解
  3. 代码膨胀:每个派生类都会实例化一个不同的基类模板

最佳实践与常见陷阱

  • 只在需要零开销和编译期已知类型时使用,否则优先简单的虚函数或组合。
  • 基类中用 static_cast<Derived*>/static_cast<const Derived*> 调用派生实现;不要用 dynamic_cast
  • 避免通过基类指针/引用“多态地”存放不同派生对象(CRTP 不是为此设计)。
  • 多重继承下的 CRTP:注意菱形结构与方法可见性,必要时用 using Derived::method 暴露成员。
  • 为 CRTP 基类提供 protected 构造函数,避免被直接实例化。
  • 若需接口约束,结合 static_assert 或 Concepts,尽早在编译期报错。
  • 链式 API 返回 Derived&/const Derived&,避免返回 *this 的基类引用。

何时使用 / 不使用 CRTP

  • 使用:
    • 静态多态、热点路径需要最大化内联与去虚拟化。
    • 可复用的“mixin 特性”(日志、计数、对比、序列化接口)按需叠加。
    • 需要在编译期强制派生类提供某些接口(静态约束)。
  • 不使用:
    • 需要在运行时根据配置/输入切换具体实现。
    • 需要通过基类容器存放异构对象并统一调度。
    • 团队对模板元编程不熟,维护成本高于收益。

与虚函数多态的对比

  • 分发时机
    • 虚函数是运行时分发(需 vtable),可通过基类指针/引用统一处理异构对象。
    • CRTP 是编译时分发,要求在编译期就知道具体类型,换来零开销。
  • 扩展方式
    • 虚函数通过覆盖虚方法扩展行为。
    • CRTP 通过“静态接口”约束派生类提供实现,基类用 static_cast<Derived*> 调用。
  • 适用场景
    • 运行时需要真正的动态绑定 → 选虚函数。
    • 性能敏感、类型在编译期已知、可模板化 → 选 CRTP。

FAQ

  • 为什么叫“奇异递归”?
    • 因为基类模板的实参“递归地”引用了派生类本身,看起来很“奇怪”,但这是合法且有用的模板技巧。
  • 可以多层套 CRTP 吗?
    • 可以,但要控制复杂度,注意方法名冲突与可读性。
  • 和 Concepts 是什么关系?
    • Concepts 负责“约束”,CRTP 负责“复用 + 分发”。两者可结合使用,提升错误信息友好度与可维护性。

小结

CRTP 是 C++ 模板元编程中的重要模式,在性能敏感场景(如游戏开发、高频交易等)尤为常见。它在不引入虚函数开销的前提下提供多态式的扩展能力,适合用于“编译期就能决定行为”的问题。但需要权衡模板复杂度与可维护性,避免过度抽象。