c++ 什么是Mixin(作为一个概念)

0mkxixxg  于 2023-02-10  发布在  其他
关注(0)|答案(7)|浏览(189)

我试图理解Mixin的概念,但我似乎无法理解它是什么。我认为它是一种通过使用继承来扩展类的能力的方法。我读到过人们将其称为“抽象子类”。有人能解释一下为什么吗?
如果你能根据下面的例子解释你的答案,我将不胜感激(摘自我的一个演讲幻灯片):

o8x7eapl

o8x7eapl1#

在讨论什么是混合之前描述它试图解决的问题是有用的。假设你有一堆想法或概念要建模。它们可能在某种程度上相关,但在大多数情况下是正交的--这意味着它们可以彼此独立。现在你可以通过继承来建模,并从某个公共接口类中派生出这些概念,然后在实现该接口的派生类中提供具体的方法。
这种方法的问题在于,这种设计没有提供任何清晰直观的方法来获取这些具体类中的每一个并将它们组合在一起。
mix-in的思想是提供一组原语类,其中每个类都建模一个基本的正交概念,并且能够把它们粘在一起组成更复杂的类,这些类具有你想要的功能--有点像乐高积木。这是可扩展的,因为以后您可以向集合中添加其他原语类,而不会影响现有的原语类。
回到C++,实现这一点的技术是使用模板和继承。这里的基本思想是通过template参数提供这些构建块,将它们连接在一起。然后将它们链接在一起,例如通过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
}

你会注意到我做了一些改变从你的原始:

  • 这里真的不需要虚函数,因为我们在编译时就知道组合类的类型。
  • 我为第二个模板参数添加了一个默认的value_type,以使其使用起来不那么麻烦,这样你就不必每次粘贴一个模板时都输入<foobar, int>
  • 我们没有创建一个从这些片段继承的新类,而是使用了一个简单的typedef

注意,这只是一个简单的例子来说明混合的思想,所以它没有考虑极端情况和有趣的用法。例如,执行undo而不设置一个数字可能不会像你所期望的那样。
作为旁注,您可能会发现this article也很有用。

csga3l58

csga3l582#

我喜欢greatwolf的回答,但也要提出一点警告。
greatwolf说:“这里真的不需要虚函数,因为我们在编译时就确切地知道我们的组合类类型是什么。”不幸的是,如果以多态方式使用对象,可能会遇到一些不一致的行为。
让我从他的例子中调整main函数:

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!
}

通过使“set”函数成为虚拟函数,将调用适当的覆盖,并且不会发生上述不一致的行为。

mo49yndu

mo49yndu3#

mixin是为另一个类提供功能而设计的类,通常通过指定的类提供功能所需的基本特性。例如,考虑以下示例:
本例中的mixin提供撤销值类的set操作的功能,该功能基于参数化类(在本例中为Number类)提供的get/set功能。
另一个示例(摘自"Mixin-based programming in 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);
  }
... 
};

在这个例子中,给定一个执行遍历操作的图类,mixin提供了 * 计算顶点数 * 的功能。
通常,在C中,mixin是通过CRTP实现的,这个线程可以很好地阅读C中的mixin实现:What is 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>
{
};

这个mixin提供了对一组(层次结构)形状类的 * 异构复制 * 功能。

e1xvtsh3

e1xvtsh34#

C++中的mixin是使用Curiously Recurring Template Pattern(CRTP)表示的。This post是对其他重用技术(编译时多态性)所提供的功能的一个很好的细分。

cld4siwp

cld4siwp5#

为了理解这个概念,暂时忘记课堂,想想(最流行的)JavaScript。其中对象是方法和属性的动态数组。可以通过它们的名称作为符号或字符串文字来调用。在2018年,你将如何在标准C中实现它?* 不容易 *。但这是概念的核心。在JavaScript中,你可以添加和删除(又名混合)无论何时何地,非常重要:没有类继承。
现在到C
。标准的C有你需要的一切,在这里作为一个语句没有帮助。显然,我不会写一个脚本语言,以实现混合使用C
是的,this is a good article,但只是为了灵感。CRTP不是万能的。而且所谓的学术方法也是here,(本质上)也是基于CRTP的。
在否决这个答案之前,也许可以考虑一下我的p.o.c. code on wand box:)

ijnw1ujt

ijnw1ujt6#

在C20以前,CRTP是C中实现mixin的标准解决方案。mixin用于避免代码重复。它是一种编译时多态。
一个典型的例子是迭代器支持接口,许多函数的实现完全相同,例如C::const_iterator C::cbegin() const总是调用C::const_iterator C::begin() const

**注意:**在C++中,structclass相同,只是成员和继承在默认情况下是公共的。

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

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 */;
    }
};

从C++23开始:显式this

编译器在编译时就知道对象的具体数据类型,在C20之前根本没有办法得到这个信息。从C23开始你可以使用explicit 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 */;
    }
};

这种类型的mixin易于实现,易于理解,易于使用,并且对错别字非常健壮!它在各个方面都上级经典的CRTP。

C++20之前的历史解决方案:培训中心

使用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 */;
    }
};
  • 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
}

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

prdp8dxp

prdp8dxp7#

这与接口的工作原理相同,也许更像是一个抽象,但接口更容易在第一次获得。
它解决了许多问题,但我发现在开发中经常出现的一个问题是外部API。
你有一个用户数据库,这个数据库有一定的方式来访问它的数据。现在想象一下你有一个Facebook,它也有一定的方式来访问它的数据(API)。
在任何时候,你的应用程序都可能需要使用Facebook或你的数据库中的数据运行。所以你要做的是创建一个接口,上面写着“任何实现我的东西都肯定会有以下方法”,现在你可以在你的应用程序中实现这个接口了...
因为接口承诺实现存储库将在其中声明方法,所以您知道无论何时何地在应用程序中使用该接口,如果切换数据,它总是会有您定义的方法,因此有数据可供使用。
这种工作模式还有很多层,但本质是它很好,因为数据或其他此类持久性项目成为应用程序的重要组成部分,如果它们在您不知情的情况下发生更改,您的应用程序可能会中断:)
这是一些伪代码。

interface IUserRepository
{
    User GetUser();
}

class DatabaseUserRepository : IUserRepository
{
    public User GetUser()
    {
        // Implement code for database
    }
}

class FacebookUserRepository : IUserRepository
{
    public User GetUser()
    {
        // Implement code for facebook
    }
}

class MyApplication
{
    private User user;

    MyApplication( IUserRepository repo )
    {
        user = repo;
    }
}

// your application can now trust that user declared in private scope to your application, will have access to a GetUser method, because if it isn't the interface will flag an error.

相关问题