什么是Mixins(作为一个概念)

Sho*_*kie 70 c++ oop templates mixins

我试图了解Mixin概念,但我似乎无法理解它是什么.我看待它的方式是,它是一种通过使用继承来扩展类的功能的方法.我读过人们将它们称为"抽象子类".有谁能解释为什么?

如果您根据以下示例(从我的一个演讲幻灯片中)解释您的答案,我将不胜感激: C++ Mixin示例

gre*_*olf 116

在进入混合之前,描述它试图解决的问题是有用的.假设您有一堆想要建模的想法或概念.它们可能在某种程度上相关,但它们在大多数情况下是正交的 - 这意味着它们可以彼此独立地站立.现在,您可以通过继承对此进行建模,并将每个概念派生自一些常见的接口类.然后,您在实现该接口的派生类中提供具体方法.

这种方法的问题在于,这种设计没有提供任何清晰直观的方法来获取每个具体类并将它们组合在一起.

混合的想法是提供一堆原始类,其中每个类都模拟一个基本的正交概念,并能够将它们组合在一起,只用你想要的功能组成更复杂的类 - 有点像legos.原始类本身旨在用作构建块.这是可扩展的,因为稍后您可以将其他原始类添加到集合中,而不会影响现有的类.

回到C++,执行此操作的技术是使用模板和继承.这里的基本思想是通过模板参数提供它们来连接这些构建块.然后将它们链接在一起,例如.via typedef,以形成包含所需功能的新类型.

举个例子,假设我们想在顶部添加一个重做功能.这是它的样子:

#include <iostream>
using namespace std;

struct Number
{
  typedef int value_type;
  int n;
  void set(int v) { n = v; }
  int get() const { return n; }
};

template <typename BASE, typename T = typename BASE::value_type>
struct Undoable : public BASE
{
  typedef T value_type;
  T before;
  void set(T v) { before = BASE::get(); BASE::set(v); }
  void undo() { BASE::set(before); }
};

template <typename BASE, typename T = typename BASE::value_type>
struct Redoable : public BASE
{
  typedef T value_type;
  T after;
  void set(T v) { after = v; BASE::set(v); }
  void redo() { BASE::set(after); }
};

typedef Redoable< Undoable<Number> > ReUndoableNumber;

int main()
{
  ReUndoableNumber mynum;
  mynum.set(42); mynum.set(84);
  cout << mynum.get() << '\n';  // 84
  mynum.undo();
  cout << mynum.get() << '\n';  // 42
  mynum.redo();
  cout << mynum.get() << '\n';  // back to 84
}
Run Code Online (Sandbox Code Playgroud)

您会注意到我从您的原始版本中做了一些更改:

  • 虚拟函数在这里确实不是必需的,因为我们确切地知道我们的编写类类型在编译时是什么.
  • value_type为第二个模板参数添加了一个默认值,使其使用不那么繁琐.这样,<foobar, int>每次粘贴一块时,您都不必继续输入.
  • 不是创建从片段继承的新类,而是typedef使用简单的.

请注意,这是一个简单的例子来说明混合的想法.所以它没有考虑角落案件和有趣的用法.例如,执行一个undo没有设置数字可能不会像你期望的那样表现.

作为旁注,您可能还会发现本文很有帮助.

  • 读者注意,`void Number :: set(int)`和`int Number :: get()const`都应该是`virtual`,以便在使用`Number*`指针时获得mixin行为. (11认同)
  • 这个例子实际上非常好,我实际上读过它并且惊讶地发现它有很多意义lolol.做得好的队友.谢谢. (7认同)
  • 建议:当`Number :: value_type`已经定义时,它可以(也应该)用于`Number :: n`,`Number :: get`和`Number :: set`. (7认同)
  • 保持基类`set` 和`get` 虚拟是有意义的,因为你可以定义`void doubler(Number &amp;n){ n.set(n.get()*2); }` 并且能够使用的是可撤销和可撤销的类 (2认同)

Man*_*726 7

mixin是一个被设计用于为另一个类提供功能的类,通常通过指定的类提供功能所需的基本功能.例如,考虑一下您的示例:
在这种情况下,mixin提供了撤消值类的设置操作的功能.此可行性基于get/set参数化类(Number在您的示例中为类)提供的功能.

另一个例子(摘自" 基于Mixin的C++编程 "):

template <class Graph>
class Counting: public Graph {
  int nodes_visited, edges_visited;
public:
  Counting() : nodes_visited(0), edges_visited(0), Graph() { }
  node succ_node (node v) {
    nodes_visited++;
    return Graph::succ_node(v);
  }
  edge succ_edge (edge e) {
    edges_visited++;
    return Graph::succ_edge(e);
  }
... 
};
Run Code Online (Sandbox Code Playgroud)

在这个例子中,mixin提供了计算顶点的功能,给定了执行trasversal操作的图形类.

通常,在C++中,mixins是通过CRTP惯用法实现的.这个主题可以很好地理解C++中的mixin实现:什么是C++ Mixin-Style?

这是一个利用CRTP习语的mixin的例子(感谢@Simple):

#include <cassert>
#ifndef NDEBUG
#include <typeinfo>
#endif

class shape
{
public:
    shape* clone() const
    {
        shape* const p = do_clone();
        assert(p && "do_clone must not return a null pointer");
        assert(
            typeid(*p) == typeid(*this)
            && "do_clone must return a pointer to an object of the same type"
        );
        return p;
    }

private:
    virtual shape* do_clone() const = 0;
};

template<class D>
class cloneable_shape : public shape
{
private:
    virtual shape* do_clone() const
    {
        return new D(static_cast<D&>(*this));
    }
};

class triangle : public cloneable_shape<triangle>
{
};

class square : public cloneable_shape<square>
{
};
Run Code Online (Sandbox Code Playgroud)

此mixin将异构副本的功能提供给形状类的集合(层次结构).

  • 这个例子根本不是CRTP. (7认同)

Ben*_*uch 6

在 C++20 之前,CRTP是在 C++ 中实现 mixins 的标准解决方法。mixin可以避免代码重复它是一种编译时多态性。

一个典型的例子是迭代器支持接口。许多功能的实现完全相同。例如,C::const_iterator C::cbegin() const总是调用C::const_iterator C::begin() const.

注意:在 C++ 中struct与 相同class,只是成员和继承默认是公共的。

struct C {
    using const_iterator = /* C specific type */;

    const_iterator begin() const {
        return /* C specific implementation */;
    }

    const_iterator cbegin() const {
        return begin(); // same code in every iterable class
    }
};
Run Code Online (Sandbox Code Playgroud)

C++ 尚未提供对此类默认实现的直接支持。但是,当cbegin()移动到基类时B,它没有有关派生类的类型信息C

struct B {
    // ???: No information about C!
    ??? cbegin() const {
        return ???.begin();
    }
};

struct C: B {
    using const_iterator = /* C specific type */;

    const_iterator begin() const {
        return /* C specific implementation */;
    }
};
Run Code Online (Sandbox Code Playgroud)

从 C++23 开始:显式this

编译器在编译时就知道对象的具体数据类型,直到 C++20 之前根本没有办法获取此信息。从 C++23 开始,您可以使用显式 this ( P0847 ) 来获取它。

struct B {
    // 1. Compiler can deduce return type from implementation
    // 2. Compiler can deduce derived objects type by explicit this
    decltype(auto) cbegin(this auto const& self) {
        return self.begin();
    }
};

struct C: B {
    using const_iterator = /* C specific type */;

    const_iterator begin() const {
        return /* C specific implementation */;
    }
};
Run Code Online (Sandbox Code Playgroud)

这种类型的 mixin 易于实现、易于理解、易于使用,并且对拼写错误具有强大的鲁棒性!它在各个方面都优于经典的 CRTP。

C++20 之前的历史解决方法:CRTP

使用 CRTP,您可以将派生类的数据类型作为模板参数传递给基类。因此,基本上相同的实现是可能的,但语法更难以理解。

// This was CRTP used until C++20!
template <typename T>
struct B {
    // Compiler can deduce return type from implementation
    decltype(auto) cbegin() const {
        // We trust that T is the actual class of the current object
        return static_cast<T const&>(*this).begin();
    }
};

struct C: B<C> {
    using const_iterator = /* C specific type */;

    const_iterator begin() const {
        return /* C specific implementation */;
    }
};
Run Code Online (Sandbox Code Playgroud)

CRTP 复杂且容易出错

此外,这里很快就会发生非常严重的拼写错误。我将稍微修改一下示例,以使错误的后果更加明显。

#include <iostream>

struct C;
struct D;

template <typename T>
struct B {
    decltype(auto) cget() const {
        return static_cast<T const&>(*this).get();
    }
};

struct C: B<C> {
    short port = 80;

    short get() const {
        return port;
    }
};

// Copy & Paste BUG: should be `struct D: B<**D**>`
struct D: B<C> {
    float pi = 3.14159265359f;

    float get() const {
        return pi;
    }
};

int main () {
    D d;

    // compiles fine, but calles C::get which interprets D::pi as short
    std::cout << "Value: " << d.cget() << '\n';
    // prints 'Value: 4059' on my computer
}
Run Code Online (Sandbox Code Playgroud)

这是一个非常危险的错误,因为编译器无法检测到它!


Ken*_*Ken 5

我喜欢大狼的回答,但会提出一点谨慎.

greatwolf说:"虚拟函数在这里确实没有必要,因为我们确切地知道我们的编写类类型在编译时是什么." 不幸的是,如果您以多态方式使用对象,则可能会遇到一些不一致的行为.

让我从他的例子中调整主要功能:

int main()
{
  ReUndoableNumber mynum;
  Undoable<Number>* myUndoableNumPtr = &mynum;

  mynum.set(42);                // Uses ReUndoableNumber::set
  myUndoableNumPtr->set(84);    // Uses Undoable<Number>::set (ReUndoableNumber::after not set!)
  cout << mynum.get() << '\n';  // 84
  mynum.undo();
  cout << mynum.get() << '\n';  // 42
  mynum.redo();
  cout << mynum.get() << '\n';  // OOPS! Still 42!
}  
Run Code Online (Sandbox Code Playgroud)

通过使"set"函数为虚拟,将调用正确的覆盖并且不会发生上面的不一致行为.