Menu
CoddyTech

通过 CRTP 实现混入

本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 77 课,共 104 课。

奇异递归模板模式(CRTP)是一种技术,其中一个类继承自模板基类,并将自身作为模板参数传入。这样可以实现编译时多态,并让基类无需使用虚函数就能访问派生类成员。

基本的 CRTP 结构如下:

template <typename Derived>
class Base {
public:
    void interface() {
        static_cast<Derived*>(this)->implementation();
    }
};

class MyClass : public Base<MyClass> {
public:
    void implementation() {
        std::cout << "MyClass implementation\n";
    }
};

CRTP 在创建混入方面尤其强大。混入是可重复使用的功能,可以“混入”类中。与传统继承不同,混入可以添加功能,而不会形成深层次的继承层级:

template <typename Derived>
class Printable {
public:
    void print() const {
        const Derived& self = static_cast<const Derived&>(*this);
        std::cout << self.toString() << "\n";
    }
};

class Person : public Printable<Person> {
    std::string name;
public:
    Person(const std::string& n) : name(n) {}
    std::string toString() const { return "Person: " + name; }
};

// 用法:
Person p("Alice");
p.print();  // 输出:Person: Alice

主要优势是,所有方法调用都会在编译时解析,从而消除虚函数调用的开销。你可以组合多个 CRTP 混入来组合功能;当类型在编译时已知时,这是一种灵活的运行时多态替代方案。

challenge icon

挑战

简单

让我们构建一个日志系统,使用 CRTP 混入为不同的类添加可复用的功能,同时避免虚函数开销。你将创建两个可以“混入”任意类的混入模板:一个用于统计实例数量,另一个用于生成字符串表示。

你将把代码组织在三个文件中:

  • Mixins.h:定义两个 CRTP 混入模板,用来提供可复用的功能。

    创建一个 Countable 混入模板,用于跟踪某个派生类有多少个实例。它应该有一个静态计数器,在构造函数中递增,在析构函数中递减。提供一个静态方法 getCount(),用于返回当前计数。

    创建一个 Describable 混入模板,用于提供 describe() 方法。这个方法应该使用 static_cast 访问派生类并调用它的 getDescription() 方法,然后打印返回结果并换行。

    请记住,CRTP 混入使用 static_cast<Derived*>(this) 在编译时访问派生类的成员。

  • Entities.h:定义两个同时继承自这两个混入的实体类。

    创建一个 Player 类,使其继承自 Countable<Player> 和 Describable<Player>。它应该存储名称和等级,并提供一个 getDescription() 方法,返回格式如下的字符串:Player: [name] (Level [level])

    创建一个 Enemy 类,也使其继承自这两个混入。它应该存储类型和生命值,并提供一个 getDescription() 方法,返回:Enemy: [type] with [health] HP

    别忘了为每个类初始化静态计数器。

  • main.cpp:读取四个输入(每个输入单独占一行):
    1. 玩家名称(字符串)
    2. 玩家等级(整数)
    3. 敌人类型(字符串)
    4. 敌人生命值(整数)

    使用给定的值创建一个 Player 和一个 Enemy。然后演示这些混入的功能:

    1. 使用静态 getCount() 方法打印 Player count: [count]
    2. 打印 Enemy count: [count]
    3. 对玩家调用 describe()
    4. 对敌人调用 describe()
    5. 创建第二个玩家,名称为 "Guest",等级为 1
    6. 再次打印 Player count: [count],显示更新后的计数
    7. 对第二个玩家调用 describe()

例如,输入为 Hero、10、Dragon 和 500 时:

Player count: 1
Enemy count: 1
Player: Hero (Level 10)
Enemy: Dragon with 500 HP
Player count: 2
Player: Guest (Level 1)

这个挑战展示了 CRTP 混入如何在不使用虚函数的情况下,为互不相关的类添加功能(计数和描述)。Player 和 Enemy 都通过继承相同的混入模板获得相同的功能,但由于模板使用不同的类型进行实例化,每个类都有独立的实例计数器。

亲手试试

#include <iostream>
#include <string>
#include "Entities.h"

using namespace std;

int main() {
    // 读取输入
    string playerName;
    int playerLevel;
    string enemyType;
    int enemyHealth;
    
    cin >> playerName;
    cin >> playerLevel;
    cin >> enemyType;
    cin >> enemyHealth;
    
    // TODO: 用给定的名字和等级创建一个 Player 对象
    
    // TODO: 用给定的类型和生命值创建一个 Enemy 对象
    
    // TODO: 使用 Player::getCount() 输出 "Player count: [count]"
    
    // TODO: 使用 Enemy::getCount() 输出 "Enemy count: [count]"
    
    // TODO: 对玩家调用 describe()
    
    // TODO: 对敌人调用 describe()
    
    // TODO: 创建第二个玩家,名字为 "Guest",等级为 1
    
    // TODO: 再次输出 "Player count: [count]"
    
    // TODO: 对第二个玩家调用 describe()
    
    return 0;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器