我有点困惑为什么奇怪重复模板模式(CRTP)设计模式有这么多“仇恨”,例如我正在阅读“Game Programming Gems 3”,并且那里有一个名为 autoLists 的设计。这使用 CRTP 创建每种类型对象的数组。
我的问题:
为什么这是一件坏事?专门针对 AutoLists 的想法,但一般来说,关于 CRTP 的答案就足够了。
我的目的是在实体组件系统中使用它,以便我可以轻松地分离每种类型的组件。
C++ 中的继承有两个不同的目的:
Mixins(向类添加新的、插入式行为,无需重复代码)。
在这种情况下,基类本身没有什么意义——它的目的是支持新的行为,而不是用作所有子类之间的公共基类。
多态性(扩展基类中已经声明的行为)。
在这个场景中,基类为所有子类提供了一个通用接口,yada yada。
CRTP 一般用于第一个目的,virtual
用于第二个目的。
认识到两者之间的区别并不容易,需要一些练习。
有时,您可以使用两者来实现相同的目标——不同之处仅在于“多态性”是静态的(在编译时)还是动态的(在运行时)。
如果您不需要运行时多态性,那么您通常会使用 CRTP,因为它通常更快,因为编译器可以在编译时看到正在发生的事情。
也就是说,CRTP 被广泛使用,以至于我不敢说它上面有“太多的仇恨”。
我在 C++、Java 和 C# 中都广泛使用了 CRTP 及其一些变体,从“同事反馈”中我可以告诉你一件事:很多人根本不理解它,并自动对“如此过于复杂”产生敌意废话”。
在有人使用它几次之前,人们真的很难看到它的好处——就像他们看到的任何其他“复杂”“新”机制一样。
的确,有时它会被用在错误的地方,并且必须在使用时特别注意细节——但这就是任何重要工具的生命。就像多重继承一样——很多人讨厌它。但是你怎么能讨厌锤子呢?没有什么可恨的,只要在真正有益的地方正确使用它,而不仅仅是因为你可以。
首先,重新考虑是否真的需要使用它。模板基类真的需要知道确切的派生类型吗?虚拟会员还不够吗?没有它你能逃脱吗?你的情况有什么好处?它会使“更高级别的代码”更短、更易读、更明显或更可扩展或更不容易出错吗?
在很多情况下,你会发现基类不需要知道确切的派生类型,你可以用更多的几个虚方法来代替它。但这可能会使整体代码对更多用户来说更加复杂。另一方面,使用 CRTP,最终机制更 .. '自动',这有时实际上是无益的。
对于实体类,通常 CRTP 的某些变体实际上是有原因的:如果您的基础公开了一些返回“相似”对象的实用方法,您通常希望这些方法返回精炼的“MyObject*”而不是“ObjectBase*”,那就是没有它很难实现。但是,真正的问题是:这些方法真的应该在实体的基类中,而不是在“工厂”、“经理”或“存储上下文”中吗?
CRTP 对CRTP 类的使用引入了编译器无法检查的限制,即它可能不是类型安全的。以下面为例。
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() {}
virtual Base *copy() = 0;
virtual void SayHello() = 0;
};
template <typename Derived>
class BaseCopy: public Base {
public:
virtual Base *copy()
{
return new Derived(static_cast<Derived const&>(*this));
}
};
如果 Base 类的用户不知道限制使用并声明
class Bar: public BaseCopy<Bar> { public: void SayHello(void) { cout << "Hello, I am Bar\n";} };
class Foo: public BaseCopy<Bar> { public: void SayHello(void) { cout << "Hello, I am Foo\n";} };
int main(void)
{
Foo *foo = new Foo;
Base *foo2 = foo->copy(); // What is foo2?
foo->SayHello();
foo2->SayHello();
delete foo2;
delete foo;
return 0;
}
用例如编译这个。克++
g++ -Wall -g main.cpp -o CRTP-test.exe
将毫无问题地编译,但调用foo->copy();
将调用未定义的行为,因为结果将是从 Foo 构造的 Bar。
//jk