CRTP 经常用作一种纯粹的实现模式,但即使在这种情形下,它也能够影响设计:某些设计选择虽然理想,但难以实现,而当出现良好的实现技术,设计决策往往随之改变。来看看 CRTP 能够解决哪些问题。
从一个具体的实现问题开始:我们有多个类具有一些共同的代码。通常情况下,会为它们编写一个基类。但这些共同的代码实际上并不完全通用:对所有类执行的操作相同,但所使用的类型不同。真正需要的不是一个普通的基类,而是一个基类模板。这就引出了 CRTP。
一个例子是对象注册表。出于调试等目的,希望知道当前有多少个特定类型的对象存在,甚至可能需要维护这些对象的列表。我们当然不希望为每个类都手动添加注册机制,所以希望将相应的机制移到基类中。但现在出现了一个问题 —— 如果两个派生类 C 和 D 都继承自同一个基类 B,那么 B 的实例计数将是 C 和 D 实例的总数。问题并不在于基类无法确定派生类的实际类型(如果愿意承担运行时多态的开销,可以确定),而在于基类中只有一个计数器(或代码中定义的任意数量的计数器),而不同派生类的数量却是无限的。可以实现一个非常复杂、昂贵且不可移植的方案,使用运行时类型信息(RTTI),例如 typeid,来确定类名,并维护一个名称与计数器的映射表。但真正需要的是每个派生类型拥有一个独立的计数器,而实现的唯一方法就是在编译时让基类知道派生类的类型,这又回到了 CRTP:
// Example 08
template <typename D> class registry {
public:
static size_t count;
static D* head;
D* prev;
D* next;
protected:
registry() {
++count;
prev = nullptr;
next = head;
head = static_cast<D*>(this);
if (next) next->prev = head;
}
registry(const registry&) {
++count;
prev = nullptr;
next = head;
head = static_cast<D*>(this);
if (next) next->prev = head;
}
~registry() {
--count;
if (prev) prev->next = next;
if (next) next->prev = prev;
if (head == this) head = next;
}
};
template <typename D> size_t registry<D>::count(0);
template <typename D> D* registry<D>::head(nullptr);
我们将构造函数和析构函数声明为 protected,因为不希望创建注册表对象,除了由派生类创建的情况。同时,不要忘记复制构造函数也非常重要,否则编译器会生成默认的复制构造函数,它不会递增计数器或更新列表(但析构函数会执行这些操作,这将导致计数器变为负数并发生溢出)。对于每个派生类 D,其基类是 registry<D>,这是一个独立的类型,拥有自己独立的静态数据成员 count 和 head(后者是当前活跃对象列表的头部指针)。现在,需要维护活跃对象运行时注册表的类型,只需继承自 registry 即可:
// Example 08
class C : public registry<C> {
int i_;
public:
C(int i) : i_(i) {}
};
第9章中,可以找到另一个类似的例子,其中基类需要知道派生类的类型,并使用该类型来声明自身的成员。
接下来,将看到另一个 CRTP 的示例,这种实现方式的可用性为某种特定的设计选择打开了大门。
另一种常常需要将行为委托给派生类的场景是访问问题。访问者是用于处理一组数据对象的实体,会依次对每个对象执行某个函数。通常,访问者本身也构成一个类型层次结构,其中派生类会定制或修改基类的某些行为。尽管访问者最常见的实现方式是使用动态多态和虚函数调用,但静态访问者能带来之前所见到的性能优势。访问者通常不会以多态方式调用 —— 创建所需的访问者并直接运行它即可。
然而,基类访问者确实会调用某些成员函数,如果派生类提供了适当的重写,这些调用可以在编译时分发(静态绑定)到派生类中。考虑以下这个用于动物集合的通用访问者示例:
// Example 09
struct Animal {
public:
enum Type { CAT, DOG, RAT };
Animal(Type t, const char* n) : type(t), name(n) {}
const Type type;
const char* const name;
};
template <typename D> class GenericVisitor {
public:
template <typename it> void visit(it from, it to) {
for (it i = from; i != to; ++i) {
this->visit(*i);
}
}
private:
D& derived() { return *static_cast<D*>(this); }
void visit(const Animal& animal) {
switch (animal.type) {
case Animal::CAT:
derived().visit_cat(animal); break;
case Animal::DOG:
derived().visit_dog(animal); break;
case Animal::RAT:
derived().visit_rat(animal); break;
}
}
void visit_cat(const Animal& animal) {
cout << "Feed the cat " << animal.name << endl;
}
void visit_dog(const Animal& animal) {
cout << "Wash the dog " << animal.name << endl;
}
void visit_rat(const Animal& animal) {
cout << "Eeek!" << endl;
}
friend D;
GenericVisitor() = default;
};
主访问方法是一个模板成员函数(模板中的模板!),可以接受能够遍历 Animal 对象序列的迭代器类型。此外,通过在类底部声明一个私有的默认构造函数,可以避免派生类在继承时错误地指定自己的类型。现在,可以开始创建一些访问者了。默认访问者简单地接受通用访问者提供的默认操作:
class DefaultVisitor :
public GenericVisitor<DefaultVisitor> {
};
可以访问 Animal 对象的序列,例如一个 vector:
std::vector<Animal> animals {
{Animal::CAT, "Fluffy"},
{Animal::DOG, "Fido"},
{Animal::RAT, "Stinky"}};
DefaultVisitor().visit(animals.begin(), animals.end());
访问操作产生预期的结果:
Feed the cat Fluffy
Wash the dog Fido
Eeek!
但是,不必局限于默认操作 —— 可以为一种或多种动物类型重写访问操作:
class TrainerVisitor :
public GenericVisitor<TrainerVisitor> {
friend class GenericVisitor<TrainerVisitor>;
void visit_dog(const Animal& animal) {
cout << "Train the dog " << animal.name << endl;
}
};
class FelineVisitor :
public GenericVisitor<FelineVisitor> {
friend class GenericVisitor<FelineVisitor>;
void visit_cat(const Animal& animal) {
cout << "Hiss at the cat " << animal.name << endl;
}
void visit_dog(const Animal& animal) {
cout << "Growl at the dog " << animal.name << endl;
}
void visit_rat(const Animal& animal) {
cout << "Eat the rat " << animal.name << endl;
}
}
当一位训狗师选择来探望动物时,可以使用 TrainerVisitor:
Feed the cat Fluffy
Train the dog Fido
Eeek!
最后,一位来访的猫咪会有自己独特的一套行为:
Hiss at the cat Fluffy
Growl at the dog Fido
Eat the rat Stinky
我们将在后面的第 17 章中,深入学习更多关于不同类型访问者的知识。而现在,将探索 CRTP 与另一种常见设计模式结合使用的场景。