【问题标题】:Curiously Recurring Template Pattern (CRTP), AutoLists and C++奇怪的重复模板模式 (CRTP)、AutoLists 和 C++
【发布时间】:2023-05-07 13:11:02
【问题描述】:

我有点困惑为什么奇怪重复模板模式 (CRTP) 设计模式有这么多“仇恨”,例如我正在阅读“游戏编程宝石 3”,其中有一个名为 autoLists 的设计.这使用 CRTP 创建每种类型对象的数组。

我的问题:

为什么这是一件坏事?专门针对 AutoLists 的想法,但一般来说,关于 CRTP 的答案就足够了。

我的目的是在实体组件系统中使用它,以便我可以轻松地分离每种类型的组件。

【问题讨论】:

  • 没有设计模式不好。 CRTP 有很多用途。然而,它有很大的被滥用的可能性——在使用它之前请确保你正确使用它!
  • 我的意图是在实体-组件系统中使用它,以便我可以轻松地分离每种类型的组件。这个好用吗?
  • 这里是对 AutoList 设计模式的描述:blog.balfes.net/2006/03/28/autolist-design-pattern
  • 我也将 CRTP 用于我的实体组件系统,如果没有它,我认为创建相同的系统会变得更加复杂。

标签: c++ crtp


【解决方案1】:

C++ 中的继承有两个不同的目的:

  1. Mixins(添加,一个类的插入行为,无需重复代码)。
    在这种情况下,基类本身没有什么意义——它的目的是支持新的行为,不是作为一个通用的基类。子类。

  2. 多态(扩展基类中已经声明的行为)。
    在这个场景中,基类为所有子类提供了一个通用接口,yada yada。

CRTP 一般用于第一个目的,virtual 用于第二个目的。
认识到两者之间的区别并不容易,需要一些练习。

有时,您可以同时使用两者来实现相同的目标——不同之处仅在于“多态性”是静态的(在编译时)还是动态的(在运行时)。
如果您不需要运行时多态性,那么您通常会使用 CRTP,因为它通常更快,因为编译器可以在编译时看到发生了什么。

也就是说,CRTP 的使用已经足够广泛,以至于我会犹豫是否要说它有“如此多的仇恨”。

【讨论】:

  • 第一点正是我的意思:) +1 的措辞非常好。
【解决方案2】:

我在 C++、Java 和 C# 中都广泛使用了 CRTP 及其一些变体,从“同事反馈”中我可以告诉你一件事:很多人根本不理解它,并自动对“这么复杂的废话”。

在有人使用它几次之前,人们真的很难看到它的好处——就像他们看到的任何其他“复杂”“新”机制一样。

确实,有时它会被用在错误的地方,而且在使用时必须格外注意细节 - 但这是任何重要工具的生命。就像多重继承一样——很多人讨厌它。但是你怎么能讨厌锤子呢?没有什么可恨的,只要在真正有益的地方正确使用它,而不仅仅是因为你可以。

首先,重新考虑是否真的需要使用它。模板基类真的需要知道确切的派生类型吗?虚拟会员还不够吗?没有它你能逃脱吗?你的情况有什么好处?它会使“更高级别的代码”更短、更易读、更明显或更可扩展或更不容易出错吗?

在很多情况下,你会发现基类不需要知道确切的派生类型,你可以用更多的几个虚方法来代替它。但这可能会使整体代码对更多用户来说更加复杂。另一方面,使用 CRTP,最终机制更 .. '自动',这有时实际上是无益的。

在实体类的情况下,CRTP 的某些变体通常有一个原因:如果您的基础公开了一些返回“相似”对象的实用方法,您通常希望这些方法返回精炼的“MyObject*”而不是“ObjectBase*” ,没有它就很难实现。但是,真正的问题是:这些方法真的应该在实体的基类中,而不是在“工厂”、“经理”或“存储上下文”中吗?

【讨论】:

  • 我希望我能接受你和@Mehrdad 的答案。因为两者都帮助我理解了对方的观点。所以我给了他赏金和你接受的答案。希望这能让你们双方都能接受奖励。
【解决方案3】:

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));
    }
};

如果基类的用户不知道限制使用并声明

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++

g++ -Wall -g main.cpp -o CRTP-test.exe

将毫无问题地编译,但调用 foo-&gt;copy(); 将调用未定义的行为,因为结果将是从 Foo 构造的 Bar。

//jk

【讨论】:

  • -1 呃,你的反例无法编译。我得到error C2440: 'static_cast' : cannot convert from 'Base&lt;Derived&gt;' to 'const Bar &amp;'
  • @Mehrdad 你是对的,我的例子过于简单和错误。我已经更新了示例。
  • 好的,我明白你现在想做什么了。有趣的! +1
  • 虽然,我应该提一下:通常,CRTP 不使用虚拟方法。您要么使用多态性,要么使用 CRTP。所以可以说,这不是一开始就写得很好的代码。 :)
  • @Mehrdad 我同意这一点。但是问题是 CRTP 是否不好使用,我的回答显示了我认为 CRTP 的主要问题。现在我承认我偶尔也会使用 CRTP,但如果你真的不知道自己在做什么,你很容易陷入困境。