Two different mixin patterns in C++. (mixin? CRTP?

2020-03-01 08:27发布

I'm studying about mixins (in C++). I read some articles on mixins and found two different patterns of "approximating" mixins in C++.

Pattern 1:

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

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

struct MyType {
};

typedef Mixin2<Mixin1<MyType>> MyTypeWithMixins;

Pattern 2: (may be called CRTP)

template<class T>
struct Mixin1 {
};

template<class T>
struct Mixin2 {
};

struct MyType {
};

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

Are they equivalent practically? I'd like to know practical difference between the patterns.

2条回答
在下西门庆
2楼-- · 2020-03-01 08:36

Are they equivalent practically? I'd like to know practical difference between the patterns.

They are different conceptually.

For the first pattern, you have decorators going (transparently) over a core functionality class, each adding their own twist/specialization to an existing implementation.

The relationship the first pattern models is "is-a" (MyTypeWithMixins is a Mixin1<MyType> specialization, Mixin1<MyType> is a MyType specialization).

This is a good approach when you are implementing functionality within a rigid interface (as all types will implement the same interface).

For the second pattern, you have functionality parts used as implementation details (possibly within different, unrelated classes).

The relationship modeled here is "is implemented in terms of" (MyTypeWithMixins is a MyType specialization, implemented in terms of Mixin1 and Mixin2 functionality). In many CRTP implementation, the CRTP templated base is inherited as private or protected.

This is a good approach when you are implementing common functionality accross different, unrelated components (i.e. not with the same interface). This is because two classes inheriting from Mixin1 will not have the same base class.

To provide a concrete example for each:

For the first case, consider the modeling of a GUI library. Each visual control would have a (for example) display function, which in a ScrollableMixin would add scroll-bars, if required; The scrollbars mixin would be a base class for most controls that are re-sizable (but all of them a part of the "control/visual component/displayable" class hierarchy.

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

In this case, all mixin types would override (for example) a display method, and delegate parts of it's functionality (the specialized drawing part) to the decorated type (the base).

For the second case, consider a case when you would implement an internal system for adding a version number to serialized objects within the application. The implementation would look like this:

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

In this case, both database_query and user_information store their settings with a version number, but they are in no way in the same object hierarchy (they don't have a common base).

查看更多
▲ chillily
3楼-- · 2020-03-01 08:48

The difference is visibility. In the first pattern, MyType's members are directly visible to and usable by the mixins, without any need for casting, and Mixin1's members are visible to Mixin2. If MyType wants to access members from the mixins, it needs to cast this, and there isn't a great way to do so safely.

In the second pattern, there is no automatic visibility between the type and the mixins, but the mixins can safely and easily cast this to MyTypeWithMixins and thereby access the members of the type and of other mixins. (MyType could too, if you applied the CRTP to it too.)

So it comes down to convenience versus flexibility. If your mixins are purely accessing services from the type, and have no sibling dependencies of their own, the first pattern is nice and straightforward. If a mixin depends on services provided by the type or other mixins, you're more or less forced to use the second pattern.

查看更多
登录 后发表回答