A mixin is a class designed to inject specific functionality into other classes through inheritance, allowing developers to add behaviors without relying on deep or rigid inheritance hierarchies. Unlike traditional base classes, mixins are often small, focused, and flexible, providing methods or properties that can be combined to enhance functionality across diverse classes. In C++, mixins are often implemented with templates and the Curiously Recurring Template Pattern (CRTP), allowing for compile-time efficiency and minimal overhead.
CRTP: Enabling Static Polymorphism
The Curiously Recurring Template Pattern (CRTP) is a C++ technique that allows for static polymorphism. In CRTP, a class inherits from a template that takes the inheriting class itself as a parameter. This way, behavior can be defined at compile-time, eliminating the runtime overhead of virtual function calls.
Example of CRTP:
template <typename Derived>
class Base {
public:
void interface() {
static_cast<Derived*>(this)->implementation();
}
};
class Derived : public Base<Derived> {
public:
void implementation() {
// Derived-specific behavior
}
};
Here, Derived inherits from Base<Derived>, allowing it to provide its own implementation at compile-time. This boosts performance by avoiding virtual calls.
Cloning Objects with CRTP-Based Mixins
Cloning objects in C++ often involves complex code with interfaces. Using CRTP, we can make a Cloneable mixin that provides cloning functionality efficiently:
template <class Derived, class Interface>
class Cloneable : public Interface {
public:
std::unique_ptr<Interface> clone() const override {
return std::make_unique<Derived>(static_cast<const Derived&>(*this));
}
};
Cloneable has two template parameters: Derived (the inheriting class) and Interface (the base interface).
The clone() method creates a new instance of Derived, ensuring all derived classes have cloning without duplicating code.
Interface:
class iShape {
public:
virtual ~iShape() = default;
public:
virtual std::unique_ptr<iShape> clone() const = 0;
};
Using Cloneable in Derived Classes:
class Circle : public Cloneable<Circle, iShape> {
public:
Circle(double r)
: radius(r)
{}
public:
Circle(const Circle& other) = default;
public:
double getRadius() const { return radius; }
private:
double radius;
};
class Square : public Cloneable<Square, iShape> {
public:
Square(double w, double h)
: width(w)
, height(h)
{}
public:
Square(const Square& other) = default;
public:
double getWidth() const { return width; }
double getHeight() const { return height; }
private:
double width;
double height;
};
Example Usage:
std::unique_ptr<iShape> original = std::make_unique<Circle>(42.0);
std::unique_ptr<iShape> copy = original->clone();
This pattern simplifies cloning in complex codebases, making object management easier and more efficient.
A more real-life example
Recently, I needed to write a simple Mesh Repair Tool. This tool is a multi-document program, where each document represents a set of geometries (mesh, voxel maps, etc.). I’ll try to describe the main entities in such a program and show how mixins helped me quickly tackle the task from an architectural perspective.
Document class
We’ll start by creating a Document class. This class will represent a storage container for all geometries and will support features like naming, assigning a unique identifier (GUID), storing objects, and copying itself.
It is important to note that the Document class owns the geometries and is responsible for their lifetime; therefore, its push_back method accepts a unique_ptr and when a document dies, all its geometries die as well.
Here’s how we can start designing this class step-by-step.
class iDocument {
public:
virtual ~iDocument() = default;
public:
virtual void setName(std::string_view newName) = 0;
virtual std::string_view getName() const = 0;
public:
virtual void setId(uuid newId) = 0;
virtual uuid getId() const = 0;
public:
virtual std::unique_ptr<iDocument> clone() const = 0;
public:
virtual void push_back(std::unique_ptr<iGeometry> geometry) = 0;
virtual iterator begin() = 0;
virtual iterator end() = 0;
// … another 30 methods from that resembles std::vector
};
DocumentManager class
Now, let’s move up a level. We need a place to store these documents. The solution: we store them in a document manager. Here, we also need all the methods similar to those in a document – searching, iterating, deleting, and adding.
Like the Document class, DocumentManager is responsible for the lifecycle of documents and manages their lifetime.
class iDocumentManager {
public:
virtual ~iDocumentManager() = default;
public:
virtual void push_back(std::unique_ptr<iDocument> document) = 0;
virtual iterator begin() = 0;
virtual iterator end() = 0;
// … another ~30 methods from that resembles std::vector
};
Finally, let’s take a look at our Geometry class. Keep in mind that my example is very simplified – this is all for ease of understanding.
In such a class, we need the ability to set a name, an ID, and to clone the geometry. Additionally, we have more unique methods specific to geometry, like selection, visibility, and setting a transformation matrix.
class iGeometry {
public:
virtual ~iGeometry() = default;
public:
virtual void setName(std::string_view newName) = 0;
virtual std::string_view getName() const = 0;
public:
virtual void setId(uuid newId) = 0;
virtual uuid getId() const = 0;
public:
virtual void setVisibility(bool value) = 0;
virtual bool getVisibility() const = 0;
public:
virtual void setSelection(bool value) = 0;
virtual bool getSelection() const = 0;
public:
virtual std::unique_ptr<iGeometry> clone() const = 0;
public:
virtual void setTransform(const glm::dmat4x4& newMatrix) = 0;
virtual const glm::dmat4x4& getTransform() const = 0;
};
Now let’s take a bird’s-eye view of all three classes and identify what they have in common. As we can see, all these interfaces share a lot of similarities, and consequently, their implementations will also have many identical methods.
This is where we finally arrive at the concept of a Mixin and how it can help us in designing Mesh Repair Program. Let’s take all the common concepts for our classes and give them names. We have something that can have name, can have id, could be cloned and behaves like vector of unique_ptrs.
Let’s call them IdMixin, NameMixin, CloneMixin, PolymorphicVectorMixin.
Then, for each Mixin(iPolymorphicVectorMixin is described later), the interface will be structured accordingly:
class iIdMixin {
public:
virtual ~iIdMixin() = default;
public:
virtual void setId(uuids::uuid newId) = 0;
virtual uuids::uuid getId() const = 0;
};
class iNameMixin {
public:
virtual ~iNameMixin() = default;
public:
virtual void setName(std::string_view newName) = 0;
virtual std::string_view getName() const = 0;
};
template<class Interface>
class iCloneMixin {
public:
virtual ~iCloneMixin() = default;
public:
virtual std::unique_ptr<Interface> clone() const = 0;
};
Let’s take a look at how the Geometry class might look when using the interfaces we discussed above.
Now, the Geometry class contains only the methods specific to it. Methods for setting the name, ID, and the clone method are declared in the corresponding interfaces.
We immediately understand what Geometry is – it’s an interface that defines operations for visibility, selection, and a transformation matrix. It’s also a class that can be named, assigned an ID, and has a clone method.
class iGeometry : public iCloneMixin<iGeometry>, public iNameMixin, public iIdMixin {
public:
virtual void setVisibility(bool value) = 0;
virtual bool getVisibility() const = 0;
public:
virtual void setSelection(bool value) = 0;
virtual bool getSelection() const = 0;
public:
virtual void setTransform(const glm::dmat4x4& newMatrix) = 0;
virtual const glm::dmat4x4& getTransform() const = 0;
};
Now, let’s see how our Document and DocumentManager interfaces will look when using IdMixin, NameMixin, CloneMixin and PolymorphicVectorMixin interfaces.
class iDocument
: public iPolymorphicVectorMixin<iGeometry>
, public iCloneMixin<iDocument>
, public iNameMixin
, public iIdMixin
{
public:
virtual ~iDocument() = default;
};
class iDocumentManager : public iPolymorphicVectorMixin<iDocument> {
public:
virtual ~iDocumentManager() = default;
};
Now our interfaces have become even simpler. They essentially don’t even contain methods. We can just look at the classes listed in the inheritance section and immediately understand what the interface represents. This is an actual code. No additional lines at all.
How to compose Mixins?
The main challenge, then, is how to compose mixins together so that each mixin implements its own interface. For example, we have a document iDocument that inherits from three mixins. Consequently, we need three implementation classes, one for each mixin. This is where CRTP comes to the rescue. We can achieve composition through inheritance. To do this, each mixin should have a template parameter – let’s call it PrevMixin. The mixin can also have other arguments, like in CloneMixin. In the end, the implementation of the IdMixin, NameMixin and CloneMixin class will look like this:
template<class PrevMixin>
class IdMixin : public PrevMixin {
public:
using PrevMixin::PrevMixin;
public:
IdMixin()
: PrevMixin()
{}
IdMixin(uuids::uuid newId)
: PrevMixin()
, id(newId)
{}
IdMixin(const IdMixin& rhs)
: PrevMixin(rhs)
, id(rhs.id)
{}
public:
void setId(uuids::uuid newId) override {
id = newId;
}
uuids::uuid getId() const override {
return id;
}
private:
uuid id{ uuid_random_generator{}() };
};
template<class PrevMixin>
class NameMixin : public PrevMixin {
public:
using PrevMixin::PrevMixin;
public:
NameMixin()
: PrevMixin()
{}
NameMixin(std::string_view newName)
: PrevMixin()
, name(newName)
{}
NameMixin(const NameMixin& rhs)
: PrevMixin(rhs)
, name(rhs.name)
{}
public:
void setName(std::string_view newName) override {
name = newName;
}
std::string_view getName() const override {
return name;
}
private:
std::string name;
};
template<class Interface, class Derived, class PrevMixin>
class CloneMixin : public PrevMixin {
public:
using PrevMixin::PrevMixin;
public:
std::unique_ptr<Interface> clone() const override {
return std::make_unique<Derived>(static_cast<const Derived&>(*this));
}
};
By writing using PrevMixin::PrevMixin; you enable the derived class to inherit all constructors from the PrevMixin class, simplifying the process of passing parameters up the mixin hierarchy. See Inheriting constructors in C++11.
Now, let’s take a look at the implementation of the Document and DocumentManager classes.
class Document final : public
IdMixin<
NameMixin<
CloneMixin<iDocument, Document, PolymorphicVectorMixin<iDocument>>>>
{
public:
};
class DocumentManager final : public PolymorphicVectorMixin<iDocumentManager> {
public:
};
This is a key point. Essentially, we don’t need to manually implement a lot of functionality. We’re not thinking in terms of code details. Instead, we’re saying – my document interface includes ID, name, and clone functionality. My interface implementation is these mixins. I don’t need to write all the setters, getters, or the clone function – they’re already there.
Now let’s take a look at the implementation of the Geometry class. It has become much simpler and easier to understand. We can clearly see which functionality it implements and what functionality is essential for it.
class Geometry : public IdMixin<NameMixin<CloneMixin<iGeometry, Geometry, iGeometry>>> {
public:
void setVisibility(bool value) override;
bool getVisibility() const override;
public:
void setSelection(bool value) override;
bool getSelection() const override;
public:
void setTransform(const glm::dmat4x4& newMatrix) override;
const glm::dmat4x4& getTransform() const override;
private:
bool visibility = true;
bool selection = false;
glm::dmat4 transform{ 1.0 };
};
PolymorphicVectorMixin
Now, let’s look at the last mixin used by both Document and DocumentManager. They both behave like vectors (of unique_ptr) – only Document holds a collection of geometries, while DocumentManager holds a collection of documents. Can we make this a mixin? Absolutely.
If we simply duplicate all vector methods and replace it with unique_ptr<iGeometry>, it won’t work as intended. We want Document to store geometry and manage its lifetime. If we take a straightforward approach, iteration would look like below, which isn’t correct.
for (std::unique_ptr<iGeometry>& geometry : document) {
}
Users shouldn’t think about how geometries are stored in document. They only knows that document (vector) manages it’s lifetime, that’s all. So basically, we want to have:
std::vector<std::unique_ptr<iGeometry>> document;
document.push_back(std::make_unique<Mesh>());
document.push_back(std::make_unique<VoxelMap>());
And we want to iterate like this:
for (iGeometry& geometry : document) {
}
In the end, we want the following interface: note that the push_back methods receive unique_ptr<T>, while the iterators behave as T&.
template<class T>
class iPolymorphicVectorMixin {
public:
using ref_value_type = std::reference_wrapper<T>;
using unique_ptr_value_type = std::unique_ptr<T>;
public:
using referance_container = std::vector<ref_value_type>;
using unique_ptr_container = std::vector<unique_ptr_value_type>;
public:
using iterator = typename referance_container::iterator;
using const_iterator = typename referance_container::const_iterator;
public:
virtual ~iPolymorphicVector() = default;
virtual iterator erase(const_iterator it) = 0;
virtual iterator insert(const_iterator pos, unique_ptr_value_type value) = 0;
public:
virtual value_type& push_back(unique_ptr_value_type value) {
return *insert(std::end(*this), std::move(value));
}
virtual void pop_back() {
erase(std::end(*this) - 1);
}
virtual void clear() {
while (!empty())
pop_back();
}
public:
virtual iterator begin() = 0;
virtual iterator end() = 0;
// and other vector like methods
};
How to achieve such behavior? The easiest (and inefficient) solution would be to store two vectors: one contains unique_ptr of T, and the other store T&. I know it’s a terrible solution, but it’s for the sake of simplifying the example.
template<class PrevMixin>
class PolymorphicVectorMixin : public PrevMixin {
public:
using PrevMixin::PrevMixin;
public:
typename PrevMixin::iterator erase(typename PrevMixin::const_iterator it) override {
auto index = std::distance(std::cbegin(refContainer), it);
auto retIt = refContainer.erase(it);
uniquePtrContainer.erase(std::cbegin(uniquePtrContainer) + index);
return retIt;
}
typename PrevMixin::iterator insert(typename PrevMixin::const_iterator pos, typename PrevMixin::unique_ptr_value_type value) override {
auto index = std::distance(std::cbegin(refContainer), pos);
uniquePtrContainer.insert(std::cbegin(uniquePtrContainer) + index, std::move(value));
return refContainer.insert(pos, *uniquePtrContainer[index]);
}
public:
typename PrevMixin::iterator begin() override {
return refContainer.begin();
}
typename PrevMixin::iterator end() override {
return refContainer.end();
}
//.... and so on
private:
typename PrevMixin::unique_ptr_container uniquePtrContainer;
typename PrevMixin::referance_container refContainer;
};
Conclusion
In this way, by using a set of mixins, we’ve eliminated the need for extensive code. Our interface is defined in a single line, and our implementation is also very straightforward. We focus on the functionality that the interface provides, as it’s broken down into atomic components. We immediately understand the functionality it represents without needing to examine each function individually. The benefits, as I see it:
- There’s practically no need to write common code;
- You simply pick what you need from a ‘menu’ of mixins;
- Even at the compilation stage, you can avoid a lot of strange bugs. A mixin either implements functionality or it doesn’t;
But there are also downsides to this approach.
- First, it can be challenging to debug if the hierarchy becomes too large;
- Second, compilation time increases due to the large number of templates;
- Third, compile-time errors can be unreadable. We all know what the error output looks like in Visual Studio;
- Fourth, mixins require self-discipline and knowledge of advanced concepts. This makes them quite challenging for beginners to use effectively;
- You need sufficient experience to separate an entity into an individual mixin;
Point 2 can be addressed with C++ modules, which should improve compile times. Point 3 can be solved with C++ concepts, which should enhance error diagnostics. Point 4 can be addressed in future with Meta Classes that Herb Sutter promotes.
P.S.
At the end of the article, I would like to demonstrate how the new C++ feature can simplify the use of the clone functionality. For example, if you have a std::unique_ptr<Circle> and you perform a clone, it would be desirable for the return type of the function to be std::unique_ptr<Circle> rather than std::unique_ptr<iShape>. For this, we will use a new feature from C++23 called deducing this.
template<class Interface>
class iCloneMixin {
public:
virtual ~iCloneMixin() = default;
private:
virtual [[nodiscard]] std::unique_ptr<Interface> cloneImpl() const = 0;
public:
template<class Self>
std::unique_ptr<Self> clone(this const Self& self) {
return std::unique_ptr<Self>{ static_cast<Self*>(self.cloneImpl().release()) };
}
};
template<class Interface, class Derived, class PrevMixin>
class CloneMixin : public PrevMixin {
public:
using PrevMixin::PrevMixin;
private:
[[nodiscard]] std::unique_ptr<Interface> cloneImpl() const override {
return std::make_unique<Derived>(static_cast<const Derived&>(*this));
}
};
Now you can use it like this:
std::unique_ptr<Circle> original = std::make_unique<Circle>(42.0);
std::unique_ptr<Circle> copy = original->clone();
P.P.S.
I also wanted to add std::is_copy_constructible_v and std::derived_from as constraints to the CloneMixin class, but it turned out that the following code does not compile:
template<class Interface, class Derived, class PrevMixin>
requires std::is_copy_constructible_v<Derived> && std::derived_from<Derived, Interface>
class CloneMixin : public PrevMixin {
public:
using PrevMixin::PrevMixin;
private:
[[nodiscard]] std::unique_ptr<Interface> cloneImpl() const override {
return std::make_unique<Derived>(static_cast<const Derived&>(*this));
}
};
Visual Studio throws the following error:
error C2139: 'Circle': an undefined class is not allowed as an argument to compiler intrinsic type trait '__is_constructible'
This essentially makes CRTP unusable with concepts. Additionally, static_assert doesn’t work inside the class:
template<class Interface, class Derived, class PrevMixin>
requires std::is_copy_constructible_v<Derived> && std::derived_from<Derived, Interface>
class CloneMixin : public PrevMixin {
public:
static_assert(std::is_copy_constructible_v<Derived> && std::derived_from<Derived, Interface>,
"Derived must have a copy constructor and must be inherited from Interface.");
// ...
};
However, it works if static_assert is moved inside a member function:
[[nodiscard]] std::unique_ptr<Interface> cloneImpl() const override {
static_assert(std::is_copy_constructible_v<Derived> && std::derived_from<Derived, Interface>,
"Derived must have a copy constructor and must be inherited from Interface.");
return std::make_unique<Derived>(static_cast<const Derived&>(*this));
}
This behavior highlights limitations in using concepts and static_assert within CRTP classes, particularly with Visual Studio.
Leave a comment