CRTP 相对于摘要 class 的优势?

Benefits of CRTP over an abstract class?

我对 'Curiously Recurring Template Pattern' 的概念不熟悉,我正在阅读它的潜在用例 here

在那篇文章中,作者描述了一个简单的案例,其中我们有两个或更多 classes 具有一些通用功能:

class A {
public:
  int function getValue() {...}
  void function setValue(int value) {...}

  // Some maths functions
  void scale() {...}
  void square() {...}
  void invert() {...}
}

class B {
public:
  double function getValue() {...}
  void function setValue(double value) {...}

  // Some maths functions
  void scale() {...}
  void square() {...}
  void invert() {...}
}

作者认为,与其在每个 class 中重复通用功能,不如使用 CRTP:

template <typename T>
struct NumericalFunctions
{
    void scale(double multiplicator);
    void square();
    void invert();
};

class A : public NumericalFunctions<A>
{
public:
    double getValue();
    void setValue(double value);
};

但我不明白的是为什么我们不能只使用抽象 class 作为通用功能并从中继承:

class NumericalFunctions
{
    virtual double function getValue() = 0;
    virtual void function setValue(double value) = 0;
    void scale(double multiplicator){...};
    void square(){...};
    void invert(){...};
};

class A : public NumericalFunctions
{
public:
    double getValue() override;
    void setValue(double value) override;
};

我想不出 CRTP 方法提供了抽象 class 没有的任何好处。有什么好处吗?抽象 class 方法对我来说似乎更简单,并且避免了在传递无效类型(未实现 getValue()setValue() 的类型)时出现无用的编译器错误消息的潜在情况CRTP 方法中的模板参数。

使用CRTP有几个优点:

  • 通用代码 - 代码库,例如容器classes、列表、数组等。在编译时评估的程序。
  • 可重用性 - 能够轻松使用现有代码创建新代码。
  • 效率 - 通用代码库的核心:对象、函数调用、它们的类型,甚至它们的值都是在编译时确定的,而不是 运行时间,与需要编译器和操作系统生成的虚函数表的运行时间开销的虚方法不同。
  • 可维护性 - 更易于维护,因为通常需要管理的代码较少,这与复杂 class 层次结构中的多重继承不同。
  • 可靠性 - 知道代码适用于不同的类型,而不必担心内部实现。
  • 可读性 - 代码的意图很明显,无需担心其内部结构。


在它们的复杂性中发现了一些缺点:

  • 语法 - 语法更难,并且要在没有任何代码味道的情况下正确编写它是一个挑战。
  • 编译器 - 根据编译器的不同,一些消息可能更加隐秘,或者如果您不真正了解编译器、生成的程序集以及在它可以将您指向实际产生或调用错误的错误代码行。
  • 链接器 - 只要代码编译正确并且对象链接正确,链接器就不应该有任何问题。
  • 调试器 - 与编译器类似,消息可能看起来更神秘,可能指向代码中实际导致或触发错误的错误位置或异常。
  • 可读性 - 由于所需语法的复杂性,阅读起来可能有点困难

确定最适合使用哪种习语的建议是根据特定代码库的需要权衡差异。

  • 如果您需要快速高效的代码,可以轻松地以最少的开销重用,那么 CRTP 将是更合适的选择。
  • 如果您需要更强大的 OOP 代码库,利用 class 具有继承、多态性以及动态和堆内存使用的层次结构,那么后者将是一个合适的选择。

然而,单个代码库可以同时使用这两种习惯用法,并将它们的优势利用到集成应用程序中。更重要的是了解何时何地需要每种类型。

以下是查看代码设计实践的正确方法的主要示例:我将使用简单的 3D 图形引擎的概念来说明这种思考、规划和开发的方法。

  • 使用 CRTP 创建自定义容器、迭代器和算法,它们是通用的、可重用的、高效的,并且在编译时生成,将存储自定义对象并对其进行处理。
  • 使用 Class 层次结构创建您将在整个代码库中使用的自定义对象、classes、结构等。

在 3D 游戏引擎中;您可能有多个容器 classes 将存储游戏的所有资产,例如称为纹理、字体、精灵、模型、着色器、音频等的图像文件。这些类型的 classes 管理打开、读取和解析这些文件并将其信息转换为您支持的自定义数据结构的功能,其核心是 CRTP,但它们可以仍然共享继承概念。

例如,个人管理器 classes 本身将以 CRTP 方式设计,以处理所有文件加载、创建、存储和清理自定义对象的内存,但它们中的大部分本身可以继承自 Singleton Base class 对象,该对象可能需要也可能不需要 subclasses 具有虚函数。 class 层次结构可能如下所示:

  • Singleton - 必须继承的抽象基础 class,它的每个派生类型都可以在每个应用程序中构造一次 运行.以下所有 classes 均来自 Singleton
    • AssetManager - 管理内部存储对象的存储和清理,这些对象可以是纹理、字体、GUI、模型、着色器或音频文件...
    • AudioManager - 管理音频播放的所有功能。
    • TextureManager - 管理不同加载纹理的实例计数,防止不必要的重复打开、读取和加载单个文件多种类型,并防止生成和存储多个同一对象的副本。
    • FontManager - 管理所有字体属性,类似于 TextureManager 但专为处理字体而设计。
    • SpriteManager - 根据引擎和游戏类型,精灵可能会或可能不会被使用,或者通常被视为纹理但属于特定类型。 ..
    • ShaderManager - 处理所有着色器以在生成的帧或场景中执行光照和着色操作。
    • GuiManager - 处理引擎支持的所有图形用户界面对象类型,例如按钮、单选按钮、滑块、列表框、复选框、文本字段、宏框等...
    • AnimationManager - 将处理和存储您的引擎支持的所有对象动画。
    • TerrainManager - 负责所有游戏的地形信息,从顶点和法线数据到高度贴图、凹凸贴图等,到彩色或图案纹理,到与地形关联的各种着色器,还可以包括天空盒或天穹、云、太阳或月亮以及背景信息。还可能包括植物,例如植物 - 草、树、灌木等...
    • ModelManager - 处理所有 3D 模型信息,为您的对象生成必要的 3D 网格,还处理其他内部数据,例如纹理坐标、索引坐标、和法线坐标。

正如您从上面看到的,每个管理器 classes 都将使用 CRTP 设计,因为它提供了一种创建通用结构的方法,可以处理许多不同的对象的类型。然而,整个 class 层次结构仍然使用继承,可能需要也可能不需要虚拟方法。后者取决于您的需要或意图。如果您希望其他人重用您的 Singleton class 来实施他们自己的个人类型的经理或其他 class 类型的 Singleton,例如 [=25] =] 和/或 ExceptionHandler 那么您可能希望要求他们必须实现 InitializationCleanup 功能。

现在关于游戏中的对象,例如将在您的 class 中使用的模型,甚至是抽象的想法,例如玩家和敌人,他们将从通用 Character 中继承class 这些将生成一个 class 层次结构,根据特定需要可能需要也可能不需要使用虚拟方法,并且这些 classes 不需要 CRTP成语。

这是为了演示何时、何地以及如何正确使用这些成语。


如果您想了解两者之间的性能差异,您最终可能要做的是编写两个代码库来执行完全相同的任务,然后专门创建一个 CRTP 而另一个没有它,只使用继承和虚函数。然后将这些代码库输入到各种在线编译器之一中,这些编译器将为可以使用的各种类型的可用编译器生成不同类型的汇编指令,并比较生成的程序集。我喜欢Compiler Explorer that is used by Jason Turner