Последнее обновление:2026-10-12
Идиома Mixin в C++¶
Всем привет, я 光城, и сегодня я поделюсь с вами одной из идиом из проекта «C++那些事»: mixin
Примеси (Mixins) — это концепция из Lisp. Примесь — это часть класса; это означает, что она предназначена для комбинирования с другими классами или примесями.
Различие между обычным самостоятельным классом (например, Person) и примесью в том, что примесь моделирует небольшие фрагменты функциональности (например, печать или отображение) и не предназначена для самостоятельного использования. Напротив, она должна объединяться с другими классами, которым нужна эта функциональность (например, с Person).
Таким образом, цель примесей — разрешить нечто похожее на множественное наследование.
Базовая запись mixin в C++ выглядит так:
или
template<typename... Super>
class Mixin : public Super... {
public:
Mixin() : Super...() {}
// ...
};
Примечание: все примеры кода из этого раздела можно получить в сообществе.
Пример 1: масштабирование и поворот¶
Предположим, мы хотим масштабировать, поворачивать прямоугольник/квадрат, добавлять рамку и т. д. — все эти операции влияют на его ширину и высоту, и мы можем реализовать их с помощью mixin.
Сначала мы можем определить обычные квадрат и прямоугольник в качестве базовых классов для mixin.
// 正方形类
class Square : public Shape {
public:
Square(int sideLength) : sideLength(sideLength) {}
int GetWidth() const override { return sideLength; }
int GetHeight() const override { return sideLength; }
private:
int sideLength;
};
// 长方形类
class Rectangle : public Shape {
public:
Rectangle(int width, int height) : width(width), height(height) {}
int GetWidth() const override { return width; }
int GetHeight() const override { return height; }
private:
int width;
int height;
};
Затем с помощью подклассов-mixin расширим функциональность. Например: для масштабирующего mixin мы можем определить собственный класс, ширина и высота которого равны scale, умноженному на соответствующие ширину и высоту.
// 缩放的Mixin
template <class Base, int ScaleX, int ScaleY>
class Scale : public Base {
public:
int GetScaledWidth() const { return Base::GetWidth() * ScaleX; }
int GetScaledHeight() const { return Base::GetHeight() * ScaleY; }
};
Подобные сценарии очень хорошо подходят для использования mixin.
Использование:
Пример 2: redo и undo¶
Полный пример имитирует операции redo и undo, позволяя нам переключаться туда-обратно между данными. В Redoable<Undoable<Number>> мы можем смешивать эти классы вместе и использовать.
#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'; // Output: 84
mynum.undo();
cout << mynum.get() << '\n'; // Output: 42
mynum.redo();
cout << mynum.get() << '\n'; // Output: back to 84
}
Пример 3: повторяющаяся печать¶
Приведём ещё один пример складывания конструктора: повторяющаяся печать.
Сначала есть класс, с помощью которого мы можем напечатать имя человека.
class Name
{
public:
Name(std::string firstName, std::string lastName)
: firstName_(std::move(firstName))
, lastName_(std::move(lastName)) {}
void print() const
{
std::cout << lastName_ << ", " << firstName_ << '\n';
}
private:
std::string firstName_;
std::string lastName_;
};
А что если я хочу напечатать повторно?
Мы можем сделать так: используем mixin и передаём в него класс Name.
template<typename Printable>
struct RepeatPrint : Printable
{
explicit RepeatPrint(Printable const& printable) : Printable(printable) {}
void repeat(unsigned int n) const
{
while (n-- > 0)
{
this->print();
}
}
};
template<typename Printable>
RepeatPrint<Printable> repeatPrint(Printable const& printable)
{
return RepeatPrint<Printable>(printable);
}
Использование:
В результате получаем печать, повторённую n раз.
Для каких же сценариев mixin подходит лучше всего?
- сохранение исходного класса без изменений,
- клиентский код не использует исходный класс напрямую — ему нужно обернуть его в mixin, чтобы воспользоваться расширенной функциональностью,
Стандартная библиотека¶
В стандартной библиотеке есть место, где используется техника mixin: std::nested_exception.
std::nested_exception — это полиморфный mixin-класс, который может перехватывать и хранить текущее исключение, благодаря чему можно вкладывать исключения произвольных типов друг в друга.
В исходниках видно, что nested_exception используется как базовый mixin-класс.
Ссылки¶
https://cedanet.com.au/ceda/Xc++/$mixin/mixins-in-straight-c++.php
https://www.fluentcpp.com/2017/12/12/mixin-classes-yang-crtp/
https://stackoverflow.com/questions/18773367/what-are-mixins-as-a-concept
Похожие темы¶
- Отказ от конструктора по умолчанию в C++ | C++那些事
- Идиома списков инициализации класса в C++ | C++那些事
- C++: классы-шаблоны вместо шаблонов функций | C++那些事
- Идиома copy-and-swap в C++ | C++那些事
- Внедрение зависимостей в C++ | C++那些事
- Сильнотипизированные перечисления enum class в C++ | C++那些事
- Header-Only в C++ против разделения реализации | C++那些事
- Идиома ленивых вычислений в C++ | C++那些事
- Идиома таблиц поиска в C++ | C++那些事
- Указатели на члены и информация о типах в C++ | C++那些事