Что такое миксин (как концепция)
Я пытаюсь разобраться с концепцией 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.