Ciabatta is the sandwich mixin support library. It's really small and most composable.
Initially written by Gašper Ažman for a talk at C++London.
Header-only.
This material is offered free of charge under the Apache 2 license. See LICENSE for details. Apache 2 is a permissive, GPL-compatible, non-copyleft license.
- libciabatta
We all know the guideline: don't write large classes. But sometimes you need to. Or sometimes, inheritance is the only form of composition that will do, because you need interface composition.
This is where ciabatta comes in.
Composed classes are like a sandwitch. The final composed class is the bottom slice. The sandwich rests on it. All the toppings are the mixins. And finally, there is the top slice, which finishes up a sandwich, and makes it a sandwich - the CRTP provider.
Ciabatta provides:
- The top slice that lets your mixins refer to each other
- The tools to compose mixins with minimal boilerplate
- The mixin concept: following this concept will make your mixins interoperable
Put this into your WORKSPACE
:
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
git_repository(
name = "ciabatta",
remote = "git@github.com:atomgalaxy/libciabatta.git",
tag = "release", # or version-N, look at the repo for tags
)
For using with a target:
cc_binary(
name = "my_program",
srcs = ["my_program.cpp"],
deps = [
"@ciabatta//", # for "@ciabatta//:ciabatta"
]
)
The tags will be updated when/if the repo changes.
If ciabatta is installed:
find_package(ciabatta REQUIRED)
add_executable(test_example example.cpp)
target_link_libraries(test_example PUBLIC ciabatta::ciabatta)
If installed under $PREFIX
: ($PREFIX
is a unix dir-tree base which
includes include/
, share/
etc.)
cd build
cmake -DCMAKE_PREFIX_PATH=$PREFIX ../
should do the trick.
Run something akin to the following:
mkdir build/ # don't build in the same directory
cd build
cmake -DCMAKE_INSTALL_PREFIX=$PREFIX ../ -GNinja
ninja
ninja test
ninja install
Check out this repository as a submodule in your thirdparty/
folder in your
repo, then add this to your CMakeLists.txt
:
add_subdirectory(thirdparty/ciabatta)
Simple include:
#include "ciabatta/ciabatta.hpp"
Include the stuff your mixins will need (ciabatta doesn't need any of these):
#include <iostream>
#include <utility>
#include <string>
Make the example easy: (don't do this in headers)
#define FWD(name) std::forward<decltype(name)>(name)
Ciabatta doesn't place much restriction on mixins. You can do whatever you like, pretty much, as long as you conform to the two rules:
- A ciabatta-compatible mixin is a class template that inherits publically from its only template parameter. (It is allowed to also inherit from other things.)
- It must forward all constructor arguments that it doesn't use to its base.
template <typename Base>
struct minimal_mixin : Base {
// A mixin must forward the constructor parameters it does not consume
// add the parameters to your mixin at the front, before `rest`
minimal_mixin(auto&&... rest) : Base(FWD(rest)...) {}
// the rest of the implementation here
};
That's it!
Note: Before C++20, the constructor needs to be written as
template <typename...Args>
minimal_mixin(Args&&...rest) : Base(std::forward<Args>(rest)...) {}
Because this is so common, ciabatta has a macro for you:
CIABATTA_DEFAULT_MIXIN_CTOR(minimal_mixin, Base);
A mixin is fundamentally a class fragment - it's useless if it can't refer to
other parts of the class. The top slice provides this capability through
providing the four overloads of self()
:
template <typename Base>
struct frobnicator : Base {
frobnicator(auto&&... rest) : Base(FWD(rest)...) {}
void frobnicate() {
// self() is provided by Base, so we need to mark it dependent.
// this->self() is short and obvious.
this->self().log("frobnicate.");
}
};
Of course, log()
needs to be provided by the concrete class somehow,
perhaps through a separate mixin:
template <typename Base>
struct stdout_logger : Base {
void log(auto&&... xs) const { (std::cout << ... << xs); }
};
However, if we wanted to log through std::ostream
, we would need some
state, which needs to be initialized. We do this by peeling off parameters
from the constructor call, and forwarding the rest:
template <typename Base>
struct ostream_logger : Base {
ostream_logger(std::ostream& out_, auto&&... rest)
: Base(FWD(rest)...)
, _out(&out_) {}
void log(auto&&... xs) { ((*_out) << ... << xs); }
private:
std::ostream* _out;
};
Note the private data: nothing in the composed class can touch _out
.
This means that you get back your encapsulation, instead of having your
entire class inline, with its 150+ members, and every function having
promiscuous access to everything.
We can now compose several different concrete classes based on this library of mixins.
Making an easy concrete class that doesn't need to initialize things:
struct concrete : ciabatta::mixin<concrete, stdout_logger, frobnicator> {
};
Or maybe something more complicated that does initialize things:
struct concrete2 : ciabatta::mixin<concrete2, ostream_logger, frobnicator, echoer> {
concrete2(std::ostream& out_) : mixin(out_, "my prefix") {}
};
For completeness, the test driver:
int main() {
concrete c;
c.frobnicate();
concrete2 c2{std::cerr};
c2.frobnicate();
}
Here are some more advanced techniques one can do with mixins:
Oftentimes, composed classes need to implement some abstract interface, with the various mixins providing the implementation of it. But how can we expose the fact that we are, in fact, implementing one?
We multiply-inherit from Base
and the interface, of course :)
Example
Our interface will be abstract_socket
:
struct message { /* payload */
};
struct abstract_socket {
virtual void receive(message) = 0;
virtual void send(message) = 0;
};
We should probably provide some null implementations for mocking:
template <typename Base>
struct null_sender : Base {
CIABATTA_DEFAULT_MIXIN_CTOR(null_sender, Base);
void send(message m) final {}
};
template <typename Base>
struct null_receiver : Base {
CIABATTA_DEFAULT_MIXIN_CTOR(null_receiver, Base);
void receive(message m) final {}
};
Let's make a mixin that adds the vtable and abstract_socket&
conversion capability:
template <typename Base>
struct is_socket : Base, abstract_socket {
CIABATTA_DEFAULT_MIXIN_CTOR(is_socket, Base);
};
And now we can make our concrete null_socket
class:
struct null_socket
: ciabatta::mixin<null_socket, null_sender, null_receiver, is_socket> {};
Note: is_socket
has to be last if you want the ability to mark member
functions final
.
If you don't want to define a new class just to inject an abstract interface, ciabatta has your back:
struct null_socket2
: ciabatta::mixin<null_socket2,
null_sender,
null_receiver,
ciabatta::mixins::provides<abstract_socket>::mixin> {};
Sometimes, like in the ciabatta::mixins::provides
mixin, we need more than
one template parameter.
This is enabled by the curry
facility. Let's take a look at a simplistic
provides
mixin:
template <typename Interface, typename Base>
struct provides : Base {
CIABATTA_DEFAULT_MIXIN_CTOR(provides, Base);
};
To use provides
as-is, we can just supply the interface
parameter to
curry
, like so:
struct null_socket3
: ciabatta::mixin<
null_socket3,
null_sender,
null_receiver,
ciabatta::curry<provides, abstract_socket>::mixin> {
};
In fact, this is what ciabatta::mixins::provides
does with its mixin
inner templated type alias.
You can run the tests with either bazel test //...
or build this package
with cmake and use ctest
in the built test directory.
the Makefile at the root of the project automates this process. It checks that once installed, the package exports the required include paths etc. by builing the tests with a "foreign" (installed) libciabatta.