The performance of messages is indeed slower than regular function calls and even virtual function calls. Even though a call's algorithmic complexity is still O(1), a number of memory indirections happen in order to call the actual method behind the message.
Unfortunately it's hard to estimate exactly how much slower the message call is. With perfect cache locality and compiler optimizations a message call will take about 15 cycles, compared to about 5-7 cycles for a virtual call and 10-12 cycles for a std::function call. 15 cycles on a modern 2.8 GHz processor take about 5 nanoseconds to execute, which is a negligible cost for a call.
Unfortunately perfect cache locality is hard to come by, and especially
hard in polymorphic code. In an amortized test with potential cache misses
for all calls, Boost.Mixin messages take on average about 120 cycles (40
nanoseconds on the same processor), compared to 90 cycles on average for
virtual calls and 115 cycles for a std::function
call.
So, generally speaking if the programmer doesn't take special care to achieve
cache locality for their object lists, a message call is about 2 times
slower than a regular method call and about 1.3 times slower than a virtual
call and about as fast as a std::function
call, which again can be called negligible.
As, we've mentioned before if cache locality is an absolutely critical
feature for the desired performance, mixin messages, virtual calls, std::function
, and other types of polymorphism
will almost certainly be detrimental for this code, and the programmers
are advised to do something else.
The message performance test compares regular method calls vs virtual
method calls vs std::function
calls vs Boost.Mixin unicast
message calls.
It creates 10,000 objects, then processes them until 10,000,000 calls are made. The actual calls are of two kinds:
The regular method calls are what you would have in cache-locality optimized setting – they are methods in an array of objects of the the same type stored by value.
The other calls are polymorphic and their arrays store objects by address that can be one of two types to simulate some cache misses in a real-world scenario with various polymorphic object types.
Here are some sample test results:
-fexceptions -g
-fexceptions -O2
Method |
Debug noop |
Debug sum |
Release noop |
Release sum |
---|---|---|---|---|
Regular |
8 ns |
18 ns |
8 ns |
8 ns |
Virtual |
22 ns |
23 ns |
15 ns |
16 ns |
|
73 ns |
129 ns |
30 ns |
34 ns |
Boost.Mixin msg |
48 ns |
72 ns |
24 ns |
35 ns |
/Od /EHsc /RTC1 /MDd /ZI
)
/O2
/Oi
/EHsc
/MD
/Gy
/Zi
)
Method |
Debug noop |
Debug sum |
Release noop |
Release sum |
---|---|---|---|---|
Regular |
33 ns |
36 ns |
9 ns |
11 ns |
Virtual |
50 ns |
55 ns |
14 ns |
14 ns |
|
427 ns |
444 ns |
9 ns |
16 ns |
Boost.Mixin msg |
232 ns |
235 ns |
14 ns |
28 ns |
The stand-alone functions generated for messages typically have an if
statement in them. It's there so as
to throw an exception if none of the mixins in an object implements the
message. If you disable the library's exceptions those if
-s will be converted to assert
-s (which in non-debug compilations
are simply ignored).
If you don't want to recompile the library with exceptions disabled,
or if you just want all other exceptions, but not these, you can disable
the throwing of exceptions from the message functions if you define
BOOST_MIXIN_NO_MSG_THROW
before including the Boost.Mixin headers.
Note that if you disable the exceptions from the message functions, calling a message on an object that doesn't implement it, will certainly lead to undefined behavior and crashes.
Also have in mind, that removing the 'if'-s will improve the performance by only a small amount of nanoseconds per message call on a modern CPU. Situations where such a thing could be significant should be very very rare.
An object mutation can be a relatively slow operation.
Every mutation will invoke all mutation rules registered within the system. Their speed may vary and will depend on whether they end up changing the mutation or not. If they do change it, some allocation may take place. Even if they don't, each of them will be invoked by a virtual function and will have at least one 'if' check (possibly more, depending on the mutation rule).
If the mutation ends up creating a new type – a mixin combination that hasn't yet been met – this will also lead to the relatively slow process of initializing the internal data structures for that type. This will lead to some allocations and loops that generate the type's call table.
Even if the mutation doesn't generate a new type, it will have to find
the existing one, which is a hash table lookup with key a bitset of size
BOOST_MIXIN_MAX_MIXINS
.
Finally, the mutation will change the object (unless it happens to be an identity mutation – adding and removing nothing). To change it it will have to allocate new mixin data for it – an array of pointers to mixins, then deallocate any mixins being removed and allocate any mixins being added, and finally deallocate the old mixin data for the object.
Using object_type_template
or same_type_mutator
will perform the first steps – the ones concerning the identification
of the object's type – only once.
To reduce the allocations for the individual object's being mutated, you can add custom allocators to some of the mixins or to the entire domain.
The mutation performance tests performs a couple of tasks to evaluate the time they consume:
same_type_mutator
.
It adds two mixins and removes one.
same_type_mutator
.
It adds a mixin with the same custom allocator.
Here are some sample test results:
-fexceptions -g
-fexceptions -O2
Task |
Mean time in Debug |
Mean time in Release |
---|---|---|
New type |
2119 ns |
682 ns |
Existing type |
1821 ns |
561 ns |
Mutate |
6635 ns |
742 ns |
Same type mutate |
1403 ns |
303 ns |
Custom allocator |
5570 ns |
632 ns |
Same type alloc |
1073 ns |
164 ns |
/Od /EHsc /RTC1 /MDd /ZI
)
/O2
/Oi
/EHsc
/MD
/Gy
/Zi
)
Task |
Mean time in Debug |
Mean time in Release |
---|---|---|
New type |
21,256 ns |
1,113 ns |
Existing type |
20,343 ns |
814 ns |
Mutate |
238,219 ns |
3,125 ns |
Same type mutate |
16,668 ns |
489 ns |
Custom allocator |
219,416 ns |
2,740 ns |
Same type alloc |
16,176 ns |
286 ns |
(For the complete, working source of this example see allocators.cpp)
Boost.Mixin allows you to set custom allocators for the persistent pieces of memory the library may require.
The library allocates some memory on initialization, which happens at a global scope – before the entry point of a program. It also has some allocations which are for instances with a very short lifetime. Currently those are not covered by the allocators.
What you can control with the custom allocators is the new memory allocated
for boost::mixin::object
instances - their internal mixin
data. You can assign a global allocator to the library and you can also set
individual allocators per mixin type.
First let's see how you can create a global allocator. Let's assume you have a couple of functions of your own that allocate and deallocate memory in some way specific to your needs:
char* allocate(size_t size); void deallocate(char* buffer);
To create a global allocator, you need to create a class derived from global_allocator
and override its virtual
methods.
class custom_allocator : public boost::mixin::global_allocator {
The first two methods allocate a buffer for the mixin data pointers. Every
object has pointers to its mixins within it. This is the array of such pointers.
The class global_allocator
has a static constant member – mixin_data_size
– which you should use to see the size of a single element in that
array.
virtual char* alloc_mixin_data(size_t count) { return allocate(count * mixin_data_size); } virtual void dealloc_mixin_data(char* ptr) { deallocate(ptr); }
The other two methods you need to overload allocate and deallocate the memory for an actual mixin class instance. As you may have already read, the buffer allocated for a mixin instance is bigger than needed because the library stores a pointer to the owning object immediately before the memory used by the mixin instance.
That's why this function is not as simple as the one for the mixin data array.
It has to conform to the mixin (and also object
pointer) alignment.
virtual void alloc_mixin(size_t mixin_size, size_t mixin_alignment, char*& out_buffer, size_t& out_mixin_offset) {
The users are strongly advised to use the static method global_allocator::calculate_mem_size_for_mixin
.
It will appropriately calculate how much memory is needed for the mixin instance
such that there is enough room at the beginning for the pointer to the owning
object and the memory alignment is respected.
size_t size = calculate_mem_size_for_mixin(mixin_size, mixin_alignment); out_buffer = allocate(size);
After you allocate the buffer you should take care of the other output parameter - the mixin offset. It calculates the offset of the actual mixin instance memory within the buffer, such that there is room for the owning object pointer in before it and all alignments are respected.
You are encouraged to use the static method global_allocator::calculate_mixin_offset
for this purpose.
out_mixin_offset = calculate_mixin_offset(out_buffer, mixin_alignment); }
The mixin instance deallocation method can be trivial
virtual void dealloc_mixin(char* ptr) { deallocate(ptr); }
To use the custom global allocator you need to instantiate it and then set
it with set_global_allocator
.
Unlike the mutation rules, the responsibility for the allocator instance
is yours. You need to make sure that the lifetime of the instance is at least
as long as the lifetime of all objects in the system.
Unfortunately this means that if you have global or static objects, you need to create a new pointer that is, in a way, a memory leak. If you do not have global or static objects, it should be safe for it to just be a local variable in your program's entry point function.
custom_allocator alloc; boost::mixin::set_global_allocator(&alloc);
As we mentioned before, you can have an allocator specific for a mixin type.
A common case for such use is to have a per-frame allocator – one that has a preallocated buffer which is used much like a stack, with its pointer reset at the end of each simulation frame (or at the beginning each new one). Let's create such an allocator.
First, a mixin instance allocator is not necessarily bound to a concrete mixin type. You can have the same instance of such an allocator set for many mixins (which would be a common use of a per-frame allocator), but for our example let's create one that is bound to an instance. We will make it a template class because the code for each mixin type will be the same.
A mixin instance allocator needs to be derived from the class mixin_allocator
. You then need to overload
its two virtual methods which are exactly the same as the mixin instance
allocation/deallocation methods in global_allocator
.
template <typename Mixin> class per_frame_allocator : public boost::mixin::mixin_allocator { private: static const size_t NUM_IN_PAGE = 1000; size_t _num_allocations; // number of "living" instances allocated const size_t mixin_buf_size; // the size of a single mixin instance buffer vector<char*> _pages; // use pages of data where each page can store NUM_IN_PAGE instances size_t _page_byte_index; // index within a memory "page" const size_t page_size; // size in bytes of a page public: // some way to obtain the instance static per_frame_allocator& instance() { static per_frame_allocator i; return i; } per_frame_allocator() : _num_allocations(0) , mixin_buf_size( calculate_mem_size_for_mixin( sizeof(Mixin), boost::alignment_of<Mixin>::value)) , page_size(mixin_buf_size * NUM_IN_PAGE) { new_memory_page(); } void new_memory_page() { char* page = new char[page_size]; _pages.push_back(page); _page_byte_index = 0; } virtual void alloc_mixin(size_t mixin_class_size, size_t mixin_alignment, char*& out_buffer, size_t& out_mixin_offset) { if(_page_byte_index == NUM_IN_PAGE) { // if we don't have space in our current page, create a new one new_memory_page(); } out_buffer = _pages.back() + _page_byte_index * mixin_buf_size; // again calculate the offset using this static member function out_mixin_offset = calculate_mixin_offset(out_buffer, mixin_alignment); ++_page_byte_index; ++_num_allocations; } virtual void dealloc_mixin(char* buf) { #if !defined(NDEBUG) // in debug mode check if the mixin is within any of our pages for(size_t i=0; i<_pages.size(); ++i) { const char* page_begin = _pages[i]; const char* page_end = page_begin + page_size; BOOST_ASSERT(buf >= page_begin && buf < page_end); } #else buf; // to skip warning for unused parameter #endif // no actual deallocation to be done // just decrement our living instances counter --_num_allocations; } // function to be called once each frame that resets the allocator void reset() { BOOST_ASSERT(_num_allocations == 0); // premature reset for(size_t i=1; i<_pages.size(); ++i) { delete[] _pages[i]; } _page_byte_index = 0; } };
Now this class can be set as a mixin allocator for a given mixin type. A side effect of the fact that it's bound to the type is that it keeps mixin instances in a continuous buffer. With some changes (to take care of potential holes in the buffer) such an allocator can be used by a subsystem that works through mixins relying on them being in a continuous buffer to avoid cache misses.
To illustrate a usage for our mixin allocator, let's imagine we have a game.
If a character in our game dies, it will be destroyed at the end of the current
frame and should stop responding to any messages. We can create a mixin called
dead_character
which implements
all those the messages with a higher priority than the rest of the mixins.
Since every object that has a dead_character
mixin will be destroyed by the end of the frame, it will be safe to use the
per-frame allocator for it.
First let's create the mixin class and sample messages:
class dead_character { public: void die() {} // ... }; BOOST_MIXIN_MESSAGE_0(void, die); BOOST_MIXIN_DEFINE_MESSAGE(die); //...
Now we define the mixin so that it uses the allocator, we just need to add
it with "&
" to
the mixin feature list, just like we add messages. There are two ways to
do so. The first one would be to do it like this:
BOOST_DEFINE_MIXIN(dead_character, ... & boost::mixin::priority(1, die_msg) & boost::mixin::allocator<per_frame_allocator<dead_character>>());
This will create the instance of the allocator internally and we won't be
able to get it. Since in our case we do care about the instance because we
want to call its reset
method,
we could use an alternative way, by just adding an actual instance of the
allocator to the feature list:
BOOST_DEFINE_MIXIN(dead_character, /*...*/ boost::mixin::priority(1, die_msg) & per_frame_allocator<dead_character>::instance());
If we share a mixin instance allocator between multiple mixins, the second way is also the way to go.
Now all mixin allocations and deallocations will pass through our mixin allocator:
boost::mixin::object o; boost::mixin::mutate(o) .add<dead_character>(); boost::mixin::mutate(o) .remove<dead_character>(); // safe because we've destroyed all instances of `dead_character` per_frame_allocator<dead_character>::instance().reset();
Currently the maximum number of arguments you can have in a message is:
4
There simply is no message declaration macro for messages with more. If you need macros for messages with more arguments, you can do so, without having to rebuild the library.
In your Boost.Mixin installation, in the gen
directory you will see a file, named arity
.
It is a text file with a single number in it. Edit the file, setting the
number to whichever value you need. Then run the script gen_message_macros.rb
(you
will need a Ruby interpreter to do so). It will generate the file include/boost/mixin/gen/message_macros.ipp
with
macros for messages with 0 to arity arguments.
If you use an include directory for Boost.Mixin diferent from the one in your installation, you will have to manually copy the newly generated message macros file over the one you use.
As long as the library itself is dynamic (.dll
on Windows or .so
on Unix or Linux) its safe to use in
an application that has dynamic libraries which use Boost.Mixin.
An interesting thing which you can accomplish with the library is to have
optional plugins – dynamic libraries that aren't linked with the executable
but may or may not be present, and if they are, they are being loaded dynamically
(with LoadLibrary
or dlopen
).
Such plugin may add a mutation rule for its special mixins, or export functions that mutate objects.
For example this may be very useful for an engineering CAD system that could
potentially have many different optional plugins for its different needs.
Say, a plugin that extends the buildings with electrical wiring could simply
mutate objects, adding a mixin mixin called electrical_wiring
that contains the appropriate functionality.
There is only one thing you need to remember when you're exporting mixins
or messages from a dynamic library: to use the export macros: BOOST_DECLARE_EXPORTED_MIXIN
and BOOST_MIXIN_EXPORTED_xxx_MESSAGE_N
. They
are exactly like their regular counterparts but for their first argument,
which is the compiler specific export symbol (__declspec(dllexport)
for Visual C++ or just BOOST_SYMBOL_EXPORT
if you're using Boost).
One of the examples that come with the library illustrates how you can have the two types of dynamic libraries – one which you link with, and one plugin.
(For the complete, working source of this example see serialization.cpp)
Of course dealing with objects in a complex system means also having some
way to save and load them. Be it from a file, database, or through a network.
We did mention serialization before in our examples but we never gave an
example of how you can serialize boost::mixin::object
-s.
The main issue would be how to convert the object's type information to a string and then use this string to create an object with the same type information. As for the concrete serialization of the data within the mixins, this example will only offer a very naïve approach. We don't recommend that you use such an approach for your mixins as it is not safe, and not backward compatible. We only use it here because it's very easy to write and the focus of this example is on the object type information.
Basically what we'll do here to save and load the data inside the mixins
is rely on two multicast messages – save
and load
– which will
have a given priority for each mixin. Thus ensuring the order of execution
to be the same in loading as it is in saving. Each save and load method of
the mixin type will assume it has the right data to save and load.
So, let's declare and define our messages.
BOOST_MIXIN_CONST_MULTICAST_MESSAGE_1(void, save, ostream&, out); BOOST_MIXIN_MULTICAST_MESSAGE_1(void, load, istream&, in); BOOST_MIXIN_DEFINE_MESSAGE(save); BOOST_MIXIN_DEFINE_MESSAGE(load);
Now let's define some simple mixins. For this example we'll assume we're writing a company management system, which has a database of key individuals – employees and clients.
class person { public: void set_name(const string& name) { _name = name; } void save(ostream& out) const; void load(istream& in); static const int serialize_priority = 1; private: string _name; }; BOOST_DEFINE_MIXIN(person, boost::mixin::priority(person::serialize_priority, save_msg) & boost::mixin::priority(person::serialize_priority, load_msg)); class employee { public: void set_position(const string& position) { _position = position; } void save(ostream& out) const; void load(istream& in); static const int serialize_priority = 2; private: string _position; }; BOOST_DEFINE_MIXIN(employee, boost::mixin::priority(employee::serialize_priority, save_msg) & boost::mixin::priority(employee::serialize_priority, load_msg)); class client { public: void set_organization(const string& position) { _organization = position; } void save(ostream& out) const; void load(istream& in); static const int serialize_priority = 3; private: string _organization; }; BOOST_DEFINE_MIXIN(client, boost::mixin::priority(client::serialize_priority, save_msg) & boost::mixin::priority(client::serialize_priority, load_msg));
Now let's declare the save and load functions for an object.
Because in this example they're stand-alone functions, like the messages,
we can't just name them save and load, because they'll clash with the message
functions defined by the message macros. So let's just name them save_obj
and load_obj
.
void save_obj(const boost::mixin::object& o, ostream& out); void load_obj(boost::mixin::object& o, istream& in);
Assuming those functions are written and work correctly, we could write some code with them like this.
First we create some objects:
boost::mixin::object e; boost::mixin::mutate(e) .add<person>() .add<employee>(); e.get<person>()->set_name("Alice Anderson"); e.get<employee>()->set_position("Programmer"); boost::mixin::object c; boost::mixin::mutate(c) .add<person>() .add<client>(); c.get<person>()->set_name("Bob Behe"); c.get<client>()->set_organization("Business Co");
Then we save them to some stream:
ostringstream out; save_obj(e, out); save_obj(c, out);
And finally use that stream to load those objects:
string data = out.str(); istringstream in(data); boost::mixin::object obj1, obj2; load_obj(obj1, in); // loading Alice load_obj(obj2, in); // loading Bob
Now, let's see what we need to do to make the code from above work as expected.
First let's write the save function.
To save the object we'll need to write the names of its mixins. You might know that mixins also have id-s, but saving id-s is not a safe operation as they are generated based on the global instantiation order. This means that different programs with the same mixins (like a client or a server), or even the same program after a recompilation, could end up generating different id-s for the same mixins.
To get the names of the mixins in an object we could use object::get_mixin_names
and it's perfectly fine, but in order to make this example a bit more interesting,
let's dive a bit into the library's internal structure.
If you've read the implementation notes or the debugging tutorial, you'll
know that an object has a type information member which contains the mixin
composition of the object in a std::vector
called _compact_mixins
. We'll
use this vector to save the mixin names.
size_t num_mixins = obj._type_info->_compact_mixins.size(); out << num_mixins << endl; // write the size for(size_t i=0; i<num_mixins; ++i) { // write each name out << obj._type_info->_compact_mixins[i]->name << endl; }
After we've stored the object type information, we can now save the data
within its mixins via the save
message from above.
save(obj, out);
That was it. Now let's move to the code of the load_obj
function.
First we need to get the number of mixins we're loading. Let's do it in this simple fashion:
string line; getline(in, line); size_t num_mixins = atoi(line.c_str());
Now we'll create an object_type_template
which we'll use to store the loaded type and give it to a new object. The
type template class (as all other mutator classes) has the method add
. Besides the way you're used to calling
it – add<mixin>()
– you can also call it with a const
char*
argument, which will be interpreted as a mixin name.
When being called like this, it will return bool
.
True if a mixin of this name was found, and false if it wasn't. Note that
this true or false value does not give you the information on whether the
mixin will be added or removed from the object, but only a mixin of name
exists in the domain. As you might remember the mutation rules (if such are
added) will determine whether the mixin is actually added an removed.
boost::mixin::object_type_template tmpl; for(size_t i=0; i<num_mixins; ++i) { getline(in, line); tmpl.add(line.c_str()); } tmpl.create();
Now what's left is to apply the template to the object:
tmpl.apply_to(obj);
The last thing we need to do when loading an object is to load the data within
its mixins. The object is created with the appropriate mixins, so let's call
the load
multicast message.
load(obj, in);
It should load the data correctly because the order of the save and load execution is the same – determined by their priority.
Here are some explanations that may help you make sense of the code of the library if you need to read it:
The overall structure of the library is based on a main class called domain
which holds all registered mixins
and messages, and keeps the type registry.
The BOOST_DEFINE_MIXIN
macro instantiates a class that is similar to a metafunction, as its only
purpose is to globally instantiate itself, which in turn will lead to
domain::register_mixin_type
being called.
It also generates a function that registers the mixin features.
domain::register_mixin_type
is a template method
and it will appropriately fill a structure, containing the mixin type information
– name, constructor, destructor, id – and will also call
the generated function that registers its features.
The feature registration is composed of two parts: one global - to introduce the feature to the domain, and local called for the specific mixin type being registered. This means that a feature is globally registered multiple times - once for each of its uses for a mixin type. The first of those times will give it an id and fill the feature information structure appropriately. The other global registrations of a feature will see that it has a valid id, and will simply skip the rest of the code.
The local feature registration is performed by the class feature_parser
that has overloads for
the supported mixin features: currently messages and allocators. The allocator
registration is simple. It just sets the allocator member in the mixin
type information structure to the appropriate value.
The message registration generates a caller function, based on the specific
mixin. This caller function is a specific instantiation of a template function
which is generated by the message declaration macros. Its template parameters
are the mixin type and the actual member function in the mixin. The caller
is then cast to void (*)()
to be stored in a vector in the mixin type information structure along
with the caller functions for all of its messages.
This process of creating a caller function is based on the article The Impossibly Fast C++ Delegates by Sergey Ryazanov.
Each newly registered mixin and message get an id. The id-s are consecutive
indexes in arrays (of mixin_type_info
and message_t
respectively)
in the domain. Thus getting the information for a mixin or a message through
its id is a O(1) operation.
The maximum numbers of registered messages and mixins are fixed through
the constants BOOST_MIXIN_MAX_MIXINS
and BOOST_MIXIN_MAX_MESSAGES
in config.hpp
.
This allows us to have fixed-size arrays for both in the domain and per object type.
A new object type is initially identified via a bitset per mixin. The domain contains an unordered (hash) map where the key is such a bitset, and the value is an object type info.
The object type info consists of such a bitset (to mark which mixins are available in this type), a compact vector of mixin type information structures a cross indexing array (to indicate which mixin data is at which position in the compact array), and a call table.
The call table plays the same role as the virtual table in C++. It's a
fixed size array for every message with non-null values for the messages
that are implemented by that type. An element of that array is of type
call_table_entry
. This
is a union that, based on whether the message is a unicast or a multicast,
will contain the message data or a begin and an end for a buffer or message
datas.
When a type is requested for an object, first it's checked whether such a combination of mixins is an existing type. If not a new object type is created. This fills the mixin information bitset, vector and cross indexing array and then fills the call table. It will allocate a single buffer for all multicast messages within that type. When filling the call table the type creation process will choose the top priority unicast messages and sort the multicasts by priority. It will throw an exception if same-priority unicasts exist.
After the type is available the object data needs to be filled. The object
consists of a pointer to its type and an array of the structure mixin_data_in_object
. This structure
wraps a simple buffer that contains the mixin instance and a pointer to
the owning object right in front of it. This is required for the need to
get the owning object from within the code of the mixin class (made through
bm_this
or object_of
). Thus, getting the owning
object from the mixin is an offset from the this
pointer.
The message calling happens through the message functions which are generated by the message declaration macros.
The call consists of the following steps:
void
(*)()
to the appropriate signature.
For multicasts there is a for
loop for the last four steps.
Many people, upon seeing Boost.Mixin for the first time, have expressed a concern with the seemingly excessive amount of macros the library's users are required to write.
The mixin definition and declaration macros are often mentioned as easy to remove, and indeed there is a way to reproduce almost all of their functionality without any macros. However not all of it can be reproduced.
One of the key features those macros provide is the global instantiation. Without them, the users will be required to provide explicit entry points for their subsystems and dynamic libraries, where they will have to call some mixin initialization functions. This is not as simple as it sounds. Here is a list of downsides that such explicit entry points may introduce:
The message macros are most likely impossible to remove. Unlike the mixin ones, each of them generates many lines of code. More than a hundred.
Probably the only way to remove them completely, would be to make the message
calls by string. This will cause the calls to make hash table look-ups
(or worse) and will prohibitively slow them down. Such a scenario will
also reflect on the way mixins are registerd. The mixin messages would
have to be set through something that resembles std::bind
adding yet more complexity to the user code.
Is is possible (and probably part of the future of the library) to create an external tool that makes the user code a bit nicer. It would resemble The Meta-Object Compiler of Qt, and similarly, would require a custom preprocessing step of the users' code.
Such a tool could theoretically solve more of the library's problems, like
the need to call message(object)
instead of object.message()
at the very least, and many more...
Still, such an approach also has many opponents, as the code you write when you use it becomes effectively not-C++, but something that can be called a C++ dialect.