Что такое миксин (как концепция)

Я пытаюсь разобраться с концепцией Mixin, но не могу понять, что это такое. На мой взгляд, это способ расширить возможности класса с помощью наследования. Я читал, что люди называют их "абстрактными подклассами". Кто-нибудь может объяснить почему?

Буду признателен, если вы объясните свой ответ на основе следующего примера (из одного из моих слайд-шоу лекций):Пример C++ Mixin

7 ответов

Решение

Прежде чем вдаваться в подробности, полезно описать проблемы, которые он пытается решить. Скажем, у вас есть куча идей или концепций, которые вы пытаетесь смоделировать. Они могут быть связаны в некотором роде, но в большинстве своем они ортогональны, то есть они могут стоять независимо друг от друга. Теперь вы можете смоделировать это с помощью наследования, и каждая из этих концепций будет получена из некоторого общего класса интерфейса. Затем вы предоставляете конкретные методы в производном классе, который реализует этот интерфейс.

Проблема с этим подходом состоит в том, что этот дизайн не предлагает какого-либо ясного интуитивного способа взять каждый из этих конкретных классов и объединить их вместе.

Идея с дополнительными модулями состоит в том, чтобы предоставить набор примитивных классов, где каждый из них моделирует базовую ортогональную концепцию, и иметь возможность соединять их вместе, чтобы составлять более сложные классы с той функциональностью, которая вам нужна - вроде как legos. Сами примитивные классы предназначены для использования в качестве строительных блоков. Это расширяется, так как позже вы можете добавить другие примитивные классы в коллекцию, не затрагивая существующие.

Возвращаясь к C++, техника для этого заключается в использовании шаблонов и наследования. Основная идея здесь заключается в том, что вы соединяете эти строительные блоки вместе, предоставляя их через параметр шаблона. Затем вы соединяете их вместе, например. с помощью 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 без установки числа, вероятно, не будет вести себя так, как вы могли бы ожидать.

В качестве заметки, вы также можете найти эту статью полезной.

Мне нравится ответ от великого волка, но я хотел бы предложить один пункт предостережения.

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

Делая виртуальную функцию "set", будет вызвано правильное переопределение, и описанное выше противоречивое поведение не произойдет.

Mixin - это класс, предназначенный для обеспечения функциональности другого класса, обычно через указанный класс, который предоставляет базовые функции, необходимые для этой функциональности. Например, рассмотрим ваш пример:
Миксин в этом случае обеспечивает функциональность отмены операции установки класса значений. Эта способность основана на get/set функциональность, предоставляемая параметризованным классом (Number класс, в вашем примере).

Другой пример (Извлечено из "Mixin-based программирования на 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);
  }
... 
};

В этом примере миксин обеспечивает функциональность подсчета вершин, учитывая класс графа, который выполняет операции обхода.

Обычно в C++ миксины реализуются через идиому CRTP. Этот поток может быть хорошим чтением о реализации mixin в C++: что такое C++ Mixin-Style?

Вот пример миксина, который использует идиому CRTP (благодаря @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>
{
};

Этот миксин обеспечивает функциональность гетерогенного копирования в набор (иерархию) классов фигур.

До C++20 CRTP был стандартным обходным путем для реализации примесей в C++. Mixin помогает избежать дублирования кода .Это своего рода полиморфизм времени компиляции.

Типичным примером являются интерфейсы поддержки итераторов. Многие функции реализованы абсолютно идентично. Например,C::const_iterator C::cbegin() constвсегда звонитC::const_iterator C::begin() const.

Примечание: в С++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
    }
};

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

Компилятор знает конкретный тип данных объекта во время компиляции, до C++20 просто не было возможности получить эту информацию. Начиная с С++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 */;
    }
};

Этот тип примеси прост в реализации, легок для понимания, прост в использовании и устойчив к опечаткам! Он превосходит классический 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 */;
    }
};

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
}

Это очень опасная ошибка, потому что компилятор не может ее обнаружить!

Миксины в C++ выражаются с использованием CURLY Recurring Template Pattern (CRTP). Этот пост является отличным описанием того, что они предоставляют по сравнению с другими методами повторного использования... полиморфизм во время компиляции.

Чтобы понять концепцию, забудьте о занятиях на мгновение. Подумайте (самый популярный) JavaScript. Где объекты - это динамические массивы методов и свойств. Может вызываться по имени в виде символа или строкового литерала. Как бы вы реализовали это в стандарте C++ в 2018 году? Не легко. Но это суть концепции. В JavaScript можно добавлять и удалять (иначе говоря, встраивать) всякий раз, когда захотите. Очень важно: нет наследования классов.

Теперь на C++. Стандартный C++ имеет все, что вам нужно, здесь не помогает утверждение. Очевидно, я не буду писать язык сценариев для реализации смешивания с использованием C++.

Да, это хорошая статья, но только для вдохновения. КРТП не панацея. А также здесь есть так называемый академический подход, также основанный на CRTP.

Перед повторным голосованием этот ответ, возможно, рассмотрите мой код Poc на коробке палочки:)

Это работает так же, как интерфейс и, может быть, больше как абстракция, но интерфейсы легче получить с первого раза.

Он решает многие проблемы, но одна из них, которую я нахожу в разработке, это внешние apis. представь это.

У вас есть база данных пользователей, эта база данных имеет определенный способ получения доступа к своим данным. Теперь представьте, что у вас есть 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.
Другие вопросы по тегам