通过 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 混入来组合功能;当类型在编译时已知时,这是一种灵活的运行时多态替代方案。
挑战
简单让我们构建一个日志系统,使用 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:读取四个输入(每个输入单独占一行):- 玩家名称(字符串)
- 玩家等级(整数)
- 敌人类型(字符串)
- 敌人生命值(整数)
使用给定的值创建一个 Player 和一个 Enemy。然后演示这些混入的功能:
- 使用静态
getCount()方法打印Player count: [count] - 打印
Enemy count: [count] - 对玩家调用
describe() - 对敌人调用
describe() - 创建第二个玩家,名称为 "Guest",等级为 1
- 再次打印
Player count: [count],显示更新后的计数 - 对第二个玩家调用
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;
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: C++ 在线编译器