PrevUpHomeNext

Advanced topics

Performance
Using custom allocators
Having messages with more arguments
Dynamic libraries and program plugins
Serializing objects
Implementation notes
Macros rationale

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:

  • Noop: call to a completely empty function that does nothing
  • Sum: performs a simple addition and stores the result in the object. This is used to simulate a function argument and also a this pointer indirection.

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:

  • OS: Ubuntu 13.04
  • CPU: AMD FX X8 8150
  • Compiler: g++ (Ubuntu/Linaro 4.7.3-1ubuntu1)
  • Debug compilation parameters: -fexceptions -g
  • Release compilation parameters -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

std::function

73 ns

129 ns

30 ns

34 ns

Boost.Mixin msg

48 ns

72 ns

24 ns

35 ns

  • OS: Windows 7
  • CPU: AMD FX 4100
  • Compiler: msvc 9 (Visual Studio 2008)
  • Debug compilation parameters: Standard (/Od /EHsc /RTC1 /MDd /ZI)
  • Release compilation parameters: Standard, but with no link time code generation (/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

std::function

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:

  • New type – Creation of new types. Mutating objects in such a way that each mutation triggers the creation of a new object type.
  • Existing type – Creation of objects with a type template, such that each object is created with a type, that's already been met in the system.
  • Mutate – Mutation of objects of different types with a target an existing type.
  • Same type mutate: Mutation of objects of the same type with same_type_mutator. It adds two mixins and removes one.
  • Custom allocator: Mutation of objects of different types with a mixin that has a custom allocator attached. The custom allocator preallocates pools of 100,000 mixins.
  • Same type alloc: Mutation of objects of the same type with same_type_mutator. It adds a mixin with the same custom allocator.

Here are some sample test results:

  • OS: Ubuntu 13.04
  • CPU: AMD FX X8 8150
  • Compiler: g++ (Ubuntu/Linaro 4.7.3-1ubuntu1)
  • Debug compilation parameters: -fexceptions -g
  • Release compilation parameters -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

  • OS: Windows 7
  • CPU: AMD FX 4100
  • Compiler: msvc 9 (Visual Studio 2008)
  • Debug compilation parameters: Standard (/Od /EHsc /RTC1 /MDd /ZI)
  • Release compilation parameters: Standard, but with no link time code generation (/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:

  • Get the message info through a function generated by the message definition macro
  • Get the call table entry for this message from the object's type
  • Get the mixin info and the caller function from the call table entry
  • Get the mixin pointer from the object, based on the mixin info
  • Cast the caller function from void (*)() to the appropriate signature.
  • Call the caller function for the mixin pointer.

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:

  • They will be compilation dependency "focal points": All mixins classes introduced by a subsystem would need to be visible from there, which means recompilations on every change, and more maintenance for the code.
  • The users will have to be extra careful not to add mixins to objects before their initialization is called.
  • Duplicated instances of the mixin data structures will exist in different modules (executable and dynamic libraries). In order to deal with this, the domain would need to store multiple copies of info for the same mixin. This will add a small runtime cost to the message calls and mutations.

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.


PrevUpHomeNext