C++中的两种不同混合模式(混合?CRTP?)

15

我正在学习C++中的mixin。我读了一些有关mixin的文章,发现了两种不同的在C++中“近似”mixin的模式。

模式1:

template<class Base>
struct Mixin1 : public Base {
};

template<class Base>
struct Mixin2 : public Base {
};

struct MyType {
};

typedef Mixin2<Mixin1<MyType>> MyTypeWithMixins;

模式2:(也称为CRTP)

template<class T>
struct Mixin1 {
};

template<class T>
struct Mixin2 {
};

struct MyType {
};

struct MyTypeWithMixins : 
    public MyType, 
    public Mixin1<MyTypeWithMixins>, 
    public Mixin2<MyTypeWithMixins> {
};

它们在实际中是否等价?我想知道这些模式之间的实际区别。


在更多的C++习惯用法中,它们被称为“从下混合”和“从上混合”,但是谷歌搜索显示现在没有其他人使用这些名称。 - Ryoji
2个回答

10
区别在于可见性。在第一种模式中,MyType的成员直接可见且可被mixin使用,无需任何转换,Mixin1的成员对Mixin2可见。如果MyType想要访问mixins的成员,它需要将this转换,并且没有很好的安全方式来做到这一点。
在第二种模式中,类型和mixins之间没有自动可见性,但是mixins可以安全且容易地将 this 转换为 MyTypeWithMixins,从而访问类型和其他mixins的成员。(如果对MyType也应用了CRTP,它也可以这样做)。
因此,这涉及方便性与灵活性之间的权衡。如果您的mixins纯粹访问类型提供的服务,并且没有自己的兄弟依赖关系,则第一种模式很好并且直接了当。如果一个mixin依赖于类型或其他mixins提供的服务,则您更多或多少被迫使用第二种模式。

7
它们在实践中是否等效?我想知道这两种方法之间的实际差异。
它们在概念上是不同的。 第一种模式中,装饰器会在核心功能类上(透明地)进行操作,每个装饰器都会向现有实现添加自己的扭曲/专业化。
第一种模式所建立的关系是"is-a"(MyTypeWithMixins是Mixin1 < MyType > 的特化,Mixin1 < MyType >是MyType的特化)。 当您在严格的接口内实现功能时,这是一种很好的方法(因为所有类型将实现相同的接口)。
对于第二种模式,您有作为实现细节的功能部分(可能位于不同的、不相关的类中)。 这里建模的关系是"is implemented in terms of"(MyTypeWithMixins是MyType的特化,使用Mixin1和Mixin2的功能来实现)。 在许多CRTP实现中,CRTP模板基础被继承为私有或受保护的。
当您在不同的、不相关的组件中实现常见功能时,这是一种很好的方法(即不具有相同接口的组件)。 这是因为从Mixin1继承的两个类将没有相同的基类。
为每个提供一个具体的例子:
对于第一种情况,请考虑GUI库的建模。 每个可视控件都将具有(例如)一个"display"函数,在ScrollableMixin中,如果需要,它会添加滚动条; 如果所有这些控件都是“控件/可视组件/可显示”的类层次结构的一部分,则滚动条mixin将成为大多数可调整大小的控件的基类。
class control {
    virtual void display(context& ctx) = 0;
    virtual some_size_type display_size() = 0;
};

template<typename C>class scrollable<C>: public C { // knows/implements C's API
    virtual void display(context& ctx) override {
        if(C::display_size() > display_size())
            display_with_scrollbars(ctx);
        else
            C::display(canvas);
    }
    ... 
};

using scrollable_messagebox = scrollable<messagebox>;

在这种情况下,所有混入类型都将覆盖(例如)一个显示方法,并将其功能的部分(专门的绘图部分)委托给装饰类型(基本类型)。
对于第二种情况,考虑一种情况,当您实现一个内部系统来为应用程序中的序列化对象添加版本号时。实现如下:
template<typename T>class versionable<T> { // doesn't know/need T's API
    version_type version_;
protected:
    version_type& get_version();
};

class database_query: protected versionable<database_query> {};
class user_information: protected versionable<user_information> {};

在这种情况下,database_queryuser_information都存储了带有版本号的设置,但它们不在同一个对象层次结构中(它们没有共同的基类)。

这不是CRTP的用途。当然,CRTP涉及到一般结构class Child : Parent<Child>,但整个重点在于基类——Parent<Child>知道子类Child(如何?它是一个模板参数!)Parent可以引用在子类本身中定义的东西——例如,它可以从子类的operator==创建一个operator!=。 - H Walters
@HWalters,不一定。就我上面给出的示例而言,versionable<T> 的实现不会对 T 强加任何限制(也就是说,它不需要知道有关 T 的任何信息)。这是有意为之的。 - utnapistim
我错了;使用Child来实例化Parent仅仅是为了方便基础类型的唯一标识而已。但我认为你争论的点不对——不对T施加限制是不足以证明使用CRTP的合理性的。特别是,我对你的例子有疑问;最终结果是database_query对象和user_information对象都有版本号和获取版本号成员,甚至类型都相同。那么使用CRTP注入它们有什么优势呢?(也就是说,基类确实是不同的类型,但在什么有用的情况下会有所区别呢?) - H Walters

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接