DynaMix  1.3.6
A new take on polymorphism in C++
Tutorials

Messages

(For the complete, working source of this example see messages.cpp)

For this tutorial we'll look at a simplified piece of code from an imaginary game. First let's define the mixin classes that we're going to use.

There's a mixin that's a part from every object of our game. The one that gives them a unique id. We'll also define a method, called trace that will display information about the mixin in a stream.

class has_id
{
public:
void set_id(int id) { _id = id; }
int id() const { return _id; }
void trace(ostream& out) const;
private:
int _id;
};

Next we'll define a class for an animated model. We could have other types of models in the game, but for this tutorial there's no need to define anything more.

The mixin offers us a way to set a mesh and two ways to set an animation. It has a render method and, again the trace method, to display info about this mixin.

class animated_model
{
public:
void set_mesh(const string& mesh);
void set_animation(const string& animation);
void set_animation(int anim_id);
void render() const;
void trace(ostream& out) const;
private:
string _mesh;
string _animation;
};

Now we'll define three types of mixins that will give us artificial intelligence logic for different occasions. They all share a method called think for the AI, and the familiar trace method.

class enemy_ai
{
public:
void think();
void trace(ostream& out) const;
};
class ally_ai
{
public:
void think();
void trace(ostream& out) const;
};
class stunned_ai
{
public:
void think();
void trace(ostream& out) const;
};

Now it's time to declare the messages our mixins will use. We have some methods in our classes for which there won't be any messages, since those methods aren't polymorphic. They're unique for their specific classes so it's absolutely adequate to call them by object.get<mixin>()->method(...).

So, let's start with the simplest case. The one we already used in the basic usage example.

The declaration syntax is the familiar macro DYNAMIX_MESSAGE_|N|, where |N| stands for the number of arguments the message has. The macro's arguments are coma separated: return value, message/method name, argument 1 type, argument 1 name, argument 2 type, argument 2 name, etc etc.

This simple case is covered by the messages think and set_mesh. Although set_mesh is a message that can be handled by a single class in our example, in an actual product there would be other types of "model" mixins, which would make it polymorphic. That's why we're making it a message instead of a method to be called by object.get<animated_model>()->set_mesh(somemesh)

DYNAMIX_MESSAGE_0(void, think);
DYNAMIX_MESSAGE_1(void, set_mesh, const string&, mesh);

Now it may seem that render is also a pretty simple example of a message, but there's a small difference. It's supposed to be handled by const methods. This makes it a const message and as such it has a different declaration macro – the same as before but with CONST added to it:

DYNAMIX_CONST_MESSAGE_0(void, render);

Lets see the trace method, that's present in all of our classes. If we declare a message for it in the way we talked above, only one of the mixins within an object will be able to handle it. But when we trace an object's info, we obviously would like to have the info for all of its mixins. For cases like this: where more than one of the mixins in an object is supposed to handle a message, DynaMix introduces multicast messages. You declare those by adding MULTICAST to the macro (before MESSAGE but after CONST if it's a const one)

DYNAMIX_CONST_MULTICAST_MESSAGE_1(void, trace, ostream&, out);

The last type of message there is meant for overloaded methods. For these we need message overloads.

A message overload will require you to think of a special name, that's used to refer to that message, different from the name of the method. Don't worry. The stand-alone function that's generated for the message call itself will have the appropriate name (the method's name).

The macro used for message overloads is the same as before with OVERLOAD at the end. The other difference is that its first argument should be the custom name for the message (followed by the type, method name, and method/message arguments like before).

In our case set_animation has two overloads:

DYNAMIX_MESSAGE_1_OVERLOAD(set_anim_by_name, void, set_animation, const string&, animation);
DYNAMIX_MESSAGE_1_OVERLOAD(set_anim_by_id, void, set_animation, int, anim_id);

As you might have guessed, any message could be defined as a message overload and indeed in the case where there are no overloads DYNAMIX_MESSAGE_N(ret, message_name, ...) will just expand to DYNAMIX_MESSAGE_N_OVERLOAD(message_name, ret, message_name, ...)

So, now that we've declared all our messages it's time to define them.

The macro used for defining a message is always the same, regardless of the message's constness, mechanism (multicast/unicast), or overload. It has a single argument – the message's name.

For the overloads we should use our custom name:

DYNAMIX_DEFINE_MESSAGE(set_anim_by_name);
DYNAMIX_DEFINE_MESSAGE(set_anim_by_id);

Great! Now that we have our messages it's time to define the classes from above as mixins.

Normally if our program is spread across several files, you should use DYNAMIX_DECLARE_MIXIN to declare that those classes are mixins, but since our program is in a single file, it can be omitted. All of its functionality is also in DYNAMIX_DEFINE_MIXIN.

We met the DYNAMIX_DEFINE_MIXIN macro from the basic example. It has two arguments – the mixin/class name and its feature list. The feature list is a ampersand separated list of symbols that represent the mixin's features. It can contain many things, but for now we'll focus on messages – the ones this mixin is supposed to handle.

The special thing here is that in order to distinguish the stand-alone function that's generated to make message calls from the message, the library defines a special symbol for each message. This symbol is used in the mixin feature list and when checking whether a mixin implements a message. The symbol is the message name postfixed with _msg.

Let's define three of our simple mixins along with their feature (message) lists:

DYNAMIX_DEFINE_MIXIN(enemy_ai, think_msg & trace_msg);
DYNAMIX_DEFINE_MIXIN(ally_ai, think_msg & trace_msg);
DYNAMIX_DEFINE_MIXIN(animated_model,
trace_msg & set_mesh_msg & set_anim_by_id_msg & set_anim_by_name_msg & render_msg);

The reason we left out has_id and stunned_ai is because we'd like to do something special with their message lists.

First, about has_id. What we'd like to do is display its info first, because the object id is usually the first thing you need about an object. So in order to achieve this, the notion of message priority is introduced. Each message in a mixin gets a priority of 0 by default. For multicast messages, like trace, the priority will affect the order in which they're executed. The higher priority a multicast message has in a mixin, the earlier it will be executed. So if we set the priority of trace in has_id to something greater than zero, we'll have a guarantee that when the object info is displayed, its id will come first.

DYNAMIX_DEFINE_MIXIN(has_id, priority(1, trace_msg));

For unicast messages the priority determines which of the potentially many mixin candidates will handle the message. Again, mixins with higher priority for a message are considered better candidates.

So if we set the priority of think in stunned_ai to something greater than zero, then adding this mixin to an object that already has a think message (like objects with enemy_ai or ally_ai), will hide it previous implementation and override it with the one from stunned_ai. If we remove the mixin, the previous implementation will be exposed and will resume handling the think calls.

Also we'll consider stunned_ai as a relatively uninteresting mixin, and set the priority of trace to -1, and make its info be displayed last (if at all available)

DYNAMIX_DEFINE_MIXIN(stunned_ai, priority(1, think_msg) & priority(-1, trace_msg));

We're now ready to start using our mixins and messages in the simplified game.

Let's start by creating two objects - an enemy and an ally to the hypothetical main character. We'll give them some irrelevant id-s and meshes.

dynamix::object enemy; // just an empty dynamix::object
.add<has_id>()
.add<animated_model>()
.add<enemy_ai>();
enemy.get<has_id>()->set_id(1);
set_mesh(enemy, "spider.mesh");
trace(enemy, cout); // trace enemy data
.add<has_id>()
.add<animated_model>()
.add<ally_ai>();
ally.get<has_id>()->set_id(5);
set_mesh(ally, "dog.mesh");
trace(ally, cout); // trace ally data

Both calls to trace from above will display info about the newly constructed objects in the console.

think(enemy); // doing enemy stuff
think(ally); // doing friendly stuff
render(enemy); // drawing a hostile enemy
render(ally); // drawing a friendly ally

Now lets try stunning our enemy. We'll just add the stunned_ai mixin and, because of its special think priority, the calls to think from then on will be handled by it.

dynamix::mutate(enemy).add<stunned_ai>();
think(enemy); // don't do hostile stuff, because you're stunned
render(enemy); // drawing a stunned enemy

Now let's remove the stun effect from our enemy, by simply removing the stunned_ai mixin from the object. The handling of think by enemy_ai will resume as before.

dynamix::mutate(enemy).remove<stunned_ai>();
think(enemy); // again do hostile stuff
render(enemy); // drawing a hostile enemy

Finally, in this tutorial we'll examine another type of object. An utility one. It has no rendering but is still a part of the scene. Let's say it's a spatial tigger of some sort:

.add<has_id>()
// ...also other mixins not relevant for this tutorial
;

Now what would happen if we call render for this object. You might know that in such case an exception will be thrown: dynamix::bad_message_call. To prevent this from happening, we typically take special precautions that messages are never called for objects that don't implement them. For example we might maintain a list of all objects that do implement render only loop through it when we render the scene. This, among others, is a perfectly valid solution, but let's say that in our particular case the non-renderable objects are so few, that we would much rather pay the price of an empty message call than the one for maintaing a list of all renderable objects.

A possible, and still valid, solution is to add a mixin to all non-renderable objects which implements render with something default (in our case nothing), but another one is to use default message implementations.

You might have noticed that the render message wasn't defined when we talked about message definitions. This is not a mistake on our part but instead we kept it for later to define it another macro:

DYNAMIX_DEFINE_MESSAGE_0_WITH_DEFAULT_IMPL(void, render)
{
cout << "Rendering nothing" << endl;
}

DYNAMIX_DEFINE_MESSAGE_N_WITH_DEFAULT_IMPL where N is the number of arguments can be used to define messages in such a way that if they're called for an object that doesn't implement them, the default implementation will be called instead of an exception being thrown.

Note that you will have to copy the signature so it matches the one in the message declaration macro. Thus you will also gain access to the arguments if such exist.

The default implementation function, much like the implementation inside a mixin is a regular function. It can return values and have access to dm_this. The only thing to consider is that it will be discrarded if the object implements a message. For example while valid for a multicast, it won't be called if at least one mixin in the object implements it. Now we can safely call render for our trigger object:

render(trigger); // rendering nothing via a default message implementation

And that concludes our tutorial on messages.

Message bids

(For the complete, working source of this example see bids.cpp)

After we covered the basic features of messages: unicasts, multicasts, priorities, overloads, and default implementations, let's now delve a bit deeper. Let's focus on message bids.

For this tutorial let's suppose we're writing an RPG game. Let's define a character mixin and some messages for it:

class character
{
public:
int get_health() const
{
return _health;
}
void take_damage(int dmg)
{
_health -= dmg;
}
// ...
private:
int _health = 100;
};
DYNAMIX_CONST_MESSAGE_0(int, get_health);
DYNAMIX_MESSAGE_1(void, take_damage, int, dmg);
DYNAMIX_DEFINE_MIXIN(character, get_health_msg & take_damage_msg);

In this game the different objects would need to be rendered on the screen in some way. For this, let's define functionality to do so.

Potentially multiple mixins in our objects would need some graphical visualization. For this our rendering design will have the multicast message supply_rendering_data which will fill an output list with the graphics for each mixin which implements it:

DYNAMIX_CONST_MULTICAST_MESSAGE_1(void, supply_rendering_data, vector<string>&, out_data);

For simplicity in our example we'll just use std::string as "rendering" data.

Now we'll define a render function which takes an objects, calls the supply_rendering_data message and prints out the contents of string vector. In an real-world scenario of this sort, of course it would have some way of supplying the result to a rendering subsystem.

void render(const dynamix::object& obj)
{
vector<string> data;
supply_rendering_data(obj, data);
cout << "Rendering:\n";
for (auto& elem : data)
{
cout << '\t' << elem << '\n';
}
cout << endl;
}

The idealized rendering mixins for our example are mesh and health_bar. Meshes will represent how our object is visualized as a part of the game world, while health bars will visualize the health of a character (if the object is such) much like many RPG-s and strategy games do.

Getting the health for the object happens through the previously defined get_health message. This is a polymophic message call, because not only characters can have health in this game. Other objects might have it as well (say destructible crates or obstacles).

class mesh
{
public:
void supply_rendering_data(vector<string>& out_data) const
{
ostringstream sout;
sout << "Mesh: " << _mesh;
out_data.push_back(sout.str());
}
string _mesh; // just use std::string for simplicity
};
DYNAMIX_DEFINE_MIXIN(mesh, supply_rendering_data_msg);
class health_bar
{
public:
void supply_rendering_data(vector<string>& out_data) const
{
ostringstream sout;
sout << "Health: " << get_health(dm_this);
out_data.push_back(sout.str());
}
};
DYNAMIX_DEFINE_MIXIN(health_bar, supply_rendering_data_msg);

Now, suppose that in our game we want some way of having invisible objects. Ar first this might seem like a straight forward case. We can just creata a mixin called invisibility which implements supply_rendering_data by adding a blur (or nothing) to the output list. Like so:

class invisibility
{
public:
void supply_rendering_data(vector<string>& out_data) const
{
out_data.push_back("A blur"); // think StarCraft :)
}
};

However supply_rendering_data is a multicast message. If we don't do anything else, adding this mixin to an object will result in the output list being filled with all existing parts plus a blur (or indeed nothing). Had supply_rendering_data been a unicast message, then we could've added a bigger priority to it so it overrides the original, but priority doesn't help us to override multicasts. It just determines the order.

There are of course many solutions to our problem using what you've learned so far (for example invisibility::supply_rendering_data can be last and used to clear the ouput list, or some kind of multicast result combinator can be used which breaks the mutlicast chain), but the cleanest and indeed most optimal solution is to use bids to override the mulcitast like so:

DYNAMIX_DEFINE_MIXIN(invisibility, bid(1, supply_rendering_data_msg));

Bids are similar to message priorities. For multicasts the priority determines the order of execution when the message is called. The bid determines which messages will be executed. They will be the ones with the highest bid (or top bidders).

So in the example from above, since invisibility bids 1 for supply_rendering_data, which is higher than the default zero, if we were to add it to an object, it would override the supply_rendering_data message (unless of course some mixins with an even higher bid are in there).

Of course, since this is a multicast, if we add other mixins which implement supply_rendering_data with the same bid, 1, their implentations will also be executed along with the one from invisibility. Let's see our resulting code in action:

// create a "hero" object and add our mixins to it
.add<character>()
.add<mesh>()
.add<health_bar>();
// set some "mesh"
hero.get<mesh>()->_mesh = "hero.mesh";
// render a mesh and a health bar
render(hero);
// add invisibility
// it will override supply_rendering_data from mesh and health_bar
.add<invisibility>();
// render a blur
render(hero);

We saw how message bids can help us override multicast messages but what about unicasts. Is there a point to bids for them?

Yes there is but before we explain, let's continue with a motivating example.

Let's imagine that in our game we want a stoneskin effect. Stoneskin will cut all damage taken by an object in half and let's also (admittedly pointlessly) add the requirement that the stoneskin effect will add 10 more health points to the object.

It obvious that the stoneskin mixin would need to override take_damage and get_health to do so. So let's define our class:

class stoneskin
{
public:
void take_damage(int dmg);
int get_health() const;
};

We now have a problem. How do we implement these functions? We do need some way of transfering the newly calculated damage, or get the exisitng health. While we could write something like dm_this->get<character>()->take_damage(dmg/2);, we did mention that not only characters have health. Writing non-polymorphic code such as this won't work if we add stoneskin to a destructible object.

Unicast bids will help us in this case.

Superifically bids for unicasts work like finer grain priorities. If an object has mixins which implement the same unicast message with the same priority, the implementation with the highest bid will be executed (note the priority is the primary sort key in this case. So if one mixin implements a message with priority 10 and bid 1, and another with priority 1 and bid 1000, the first one's implenentation will be executed because it has the highest priority).

However when setting bids to unicasts, all bidders from the top priority will be available in an object which implements a message. This allows us to call a lower bidder from a higher one.

Think of this as calling the superclass's virtual method from the one that overrides it in a sublass.

If you override a unicast message by adding a mixin to an object which already implements it, when only priorities are involved, the overriden implementation is inaccessible and lost until we remove the mixin. However bids allow us to call the DYNAMIX_CALL_NEXT_BIDDER macro from a message implementation to call a lower bidder with the same priority which we have overriden.

// implement the messages with higher bids
DYNAMIX_DEFINE_MIXIN(stoneskin, bid(1, get_health_msg) & bid(1, take_damage_msg));
// call next bidders modifying values by the stoneskin effect
void stoneskin::take_damage(int dmg)
{
DYNAMIX_CALL_NEXT_BIDDER(take_damage_msg, dmg / 2);
}
int stoneskin::get_health() const
{
return DYNAMIX_CALL_NEXT_BIDDER(get_health_msg) + 10;
}

As you can see to call the next bidder, you need to supply the message tag as an argument. Otherwise the macro behaves exacly like the underlying function: it has the same arguments and the same return value.

The need to supply the message tag helps us to also call next bidders from methods which don't necessarily implement the message in question. Now let's use our unicast bids and calling of next bidders:

// hit the hero with 20 damage
take_damage(hero, 20);
// rendering hero with health 80 (from initial 100)
render(hero);
.add<stoneskin>();
// with stoneskin the health is +10
// so now we render a hero with health 90
render(hero);
// hit the hero with 50 damage
// stoneskin will transfer 25
take_damage(hero, 50);
// render hero with 80 + 10 - 25 = 65 health
render(hero);

Object mutation

(For the complete, working source of this example see mutation.cpp)

For this tutorial let's begin by introducing some mixins that may be found in a game: A root mixin, that should be present in all objects, and two that provide a way to render the object on the screen:

class game_object
{
int _id;
string name;
// ... other common fields
};
class opengl_rendering {};
class directx_rendering {};

We won't concern ourselves with their concrete functionality, so we'll just leave them with no messages.

DYNAMIX_DEFINE_MIXIN(game_object, dynamix::none);
DYNAMIX_DEFINE_MIXIN(opengl_rendering, dynamix::none);
DYNAMIX_DEFINE_MIXIN(directx_rendering, dynamix::none);

You're probably familiar from the previous examples with the most basic way to mutate an object, so let's use it to give this one a type.

.add<game_object>()
.add<opengl_rendering>();

...and then change it. Let's assume we're switching our rendering platform.

.remove<opengl_rendering>()
.add<directx_rendering>();

Using the mutate class is probably the most common way in which you'll mutate objects in DynaMix. Yes, mutate is not a function but a class. It has methods remove and add, and in its destructor it applies the actual mutation.

A mutation is a relatively slow process so if the internal object type was being changed on each call of remove or add, first the program would be needlessly slowed down, and second the library would need to deal with various incomplete types in its internal type registry.

So, if you want to add and remove mixins across several blocks or functions, you may safely instantiate the mutate class or use its typedef single_object_mutator that probably has a more appropriate name for cases like this.

mutation.remove<directx_rendering>();
// ...
mutation.add<opengl_rendering>();

Here obj1 hasn't been mutated yet. A type that has game_object and opengl_rendering hasn't been instantiated internally. In order for this to happen the mutation instance needs to be destroyed, or, to explicitly perform the mutation, you may call apply like so:

mutation.apply();

Now obj1 has been mutated, and mutation has been "cleared" – returned to the empty state it had right after its instantiation. This means we can reuse it to perform other mutation on obj1. Say:

mutation.remove<game_object>();

Oops! We're removing the mixin that needs to be present in all objects. Not to worry. You may "clear" a mutation without applying it, by calling cancel.

mutation.cancel();

Now the mutation is not performed, and its state is empty.

You may safely apply empty mutations to an object:

mutation.apply(); // no effect

Another way to mutate objects is by using a type template.

A type template gives a type to an object and, unlike mutate/single_object_mutator it's not bound to a specific object instance. Again unlike mutate it disregards all mixins within an object and applies its internal type, hence it has no remove method. It implicitly "removes" all mixins that are not among its own.

You can create a type template like so:

dynamix::object_type_template directx_rendering_template;
directx_rendering_template
.add<game_object>()
.add<directx_rendering>()
.create();

Again, similar to the case with single_object_mutator, you can spread these calls among many blocks or functions.

Don't forget to call create. It is the method that creates the internal object type. If you try to apply a type template that hasn't been created to an object, a runtime error will be triggered.

To apply a type template to an object, you may pass it as a parameter to its constructor.

dynamix::object obj2(directx_rendering_template);

Now obj2 has the mixins game_object and directx_rendering.

Let's create a new type template.

dynamix::object_type_template opengl_rendering_template;
opengl_rendering_template
.add<game_object>()
.add<opengl_rendering>()
.create();

...to illustrate the other way of applying it to an object:

opengl_rendering_template.apply_to(obj2);

Applying this type template it the same object, was equivalent to mutate-ing it, removing directx_rendering and adding opengl_rendering.

Now we have two objects – obj1 and obj2 – that have the same mixins.

Sometimes the case would be such that you have a big group of objects that have the exact same type internally, and want them all to be mutated to have a different type. Naturally you may mutate each of them one by one, and this would be the appropriate (and only) way to mutate a group of objects that have a /different/ type.

If the type is the same, however, you have a slightly faster alternative. The same type mutator:

directx
.remove<opengl_rendering>()
.add<directx_rendering>();

Unlike type templates, same type mutators don't need you to create them explicitly with some method. The creation of the internal type and all preparations are done when the mutation is applied to the first object.

directx.apply_to(obj1);
directx.apply_to(obj2);

Remember that the only time you can afford to use a same type mutator, is when /all/ objects that need to be mutated with it are composed of the same set of mixins.

Mutation rules

(For the complete, working source of this example see mutation_rules.cpp)

Let's define some mixins that may be present in a CAD system specialized for furniture design. Like in the previous example, we won't concern ourselves with any particular messages.

So, again we have a mixin that we want to be present in every object.

class furniture
{
int _id;
string name;
// ... other common fields
};
DYNAMIX_DEFINE_MIXIN(furniture, dynamix::none);

We also have mixins that describe the frame of the piece of furniture.

class wood_frame {};
DYNAMIX_DEFINE_MIXIN(wood_frame, dynamix::none);
class metal_frame {};
DYNAMIX_DEFINE_MIXIN(metal_frame, dynamix::none);

Let's also define some mixins that will be responsible for the object serialization.

class ofml_serialization {};
DYNAMIX_DEFINE_MIXIN(ofml_serialization, dynamix::none);
class xml_serialization {};
DYNAMIX_DEFINE_MIXIN(xml_serialization, dynamix::none);

And finally let's define two mixins that would help us describe our piece of furniture if it can contain objects inside – like a cabinet or a wardrobe.

class has_doors {};
DYNAMIX_DEFINE_MIXIN(has_doors, dynamix::none);
class container {};
DYNAMIX_DEFINE_MIXIN(container, dynamix::none);

Now, let's move on to the entry point of our program.

We said that each and every object in our system should be expected to have the mixin furniture. That could be accomplished if we manually add it to all mutations we make but there is a simpler way to do it. By adding the mandatory_mixin mutation rule.

All mutation rules should be added by calling add_mutation_rule. Since mandatory_mixin is a mutation rule that the library provides, we can accomplish this with a single line of code:

Now each mutation after this line will add furniture to the objects (even if it's not been explicitly added) and also if a mutation tries to remove the furniture mixin from the object, it won't be allowed. There won't be an error or a warning. The library will silently ignore the request to remove furniture, or any other mixin that's been added as mandatory. Note, that if a mutation tries to remove furniture, and also adds and removes other mixins, only the part removing the mandatory mixin will be ignored. The others will be performed.

Another common case for using mandatory_mixin is if you want to have some debugging mixin, that you want present in you objects, when you're debugging your application. This is very easily accomplished if you just set the rule for it in a conditional compilation block.

You probably noticed the mixin ofml_serialization. OFML is a format specifically designed for describing furniture that's still used in some European countries, but hasn't gotten worldwide acceptance. Let's assume we want to drop the support for OFML, but without removing the actual code, since third party plugins to our CAD system may still depend on it. All we want is to prevent anybody from adding the mixin to an object. Basically the exact opposite of mandatory_mixin. This is the mutation rule deprecated_mixin

After that line of code, any mutation that tries to add ofml_serialization won't be able to, and all mutations will try to remove it if it's present in an object. Again, as was the case before, if a mutation does many things, only the part from it, trying to add ofml_serialization will be silently ignored. Also, we will store the id of the newly added rule for the next example.

Mutation rules are registered globally and they are ran on every mutation, inadvertedly slowing it down. Sometimes you will encounter the need to add a mutation rule which is needed or only makes sense for a limited amount of time. For example deprecating ofml_serialization from the source line above might only be needed when loading objects and then our code might never add it, rendering this rule useless for the majority of the program's run. In such cases we can remove a rule with remove_mutation_rule like so:

The last built-in rule in the library is mutually_exclusive_mixins.

Since a piece of furniture has either wood frame or a metal frame and never both, it would be a good idea to prevent the programmers from accidentally adding both mixins representing the frame in a single object. This mutation rule helps us do exactly that.

You may add as many mutually exclusive mixins as you wish. If you had, say, plastic_frame, you could also add it to that list.

Any object mutated after that rule is set will implicitly remove any of the mutually exclusive mixins if another is added.

In many of our examples a sample game code was given, with mixins opengl_rendering and directx_rendering. The mutually_exclusive_mixins is perfect for this case and any other when we're always doing add<x>().remove<y>() and add<y>().remove<x>().

So to see this in practice:

This object is empty. Mutation rules don't apply if there's no mutation. If, however, the object had been created with a type template passed in its constructor, then the rules would have been applied.

.add<ofml_serialization>()
.add<xml_serialization>()
.add<wood_frame>();

Two rules are affected by this mutation. First it will implicitly add furniture to the object, and second it will ignore the attempt to add ofml_serialization. As a result the object will have furniture, xml_serialization and wood_frame.

.add<metal_frame>();

The mutually exclusive mixins will ensure that after this line the object won't have the wood_frame mixin.

Having listed some built-in mutation rules, let's now define a custom one.

Defining a custom rule is very easy. All you need to do is create a class derived from dynamix::mutation_rule and override its pure virtual method apply_to. The method has a single input-output parameter – the mutation that has been requested.

If you remember, we defined two mixins we haven't yet used – has_doors and container. We can safely say that a piece of furniture that has doors is always also a container (The opposite is not true. Think racks and bookcases). So it would be a good idea to add a mutation rule which adds the container mixin if has_doors is being added, and removes has_doors if container is being removed and the object has doors.

class container_rule : public dynamix::mutation_rule
{
public:
virtual void apply_to(dynamix::object_type_mutation& mutation)
{
if(mutation.is_adding<has_doors>())
{
mutation.start_adding<container>();
}
if(mutation.is_removing<container>() && mutation.source_has<has_doors>())
{
mutation.start_removing<has_doors>();
}
}
};

That's it. Now all we have to do is add our mutation rule and it will be active.

dynamix::add_mutation_rule(new container_rule);
.add<has_doors>();

After this mutation our custom mutation rule has also added container to the object.

.remove<container>();

And after this line, thanks to our custom mutation rule, the object o will also have its has_doors mixin removed.

To see all ways in which you can change a mutation from the mutation rule, check out the documentation entry on object_type_mutation.

Lastly, there are three more important pieces of information about mutation rules that you need to know.

In these examples we always added mutation rule pointers, allocated with new. In such case the library will take ownership of the pointer and will be responsible for destroying and deallocating the rules you've added. However you can also add mutation rules with std::shared_ptr and keep ownership even after they are removed (and potentially readd them).

Second, you may have noticed that mutation rules can logically depend on each other. You may ask yourselves what does the library do about that? Does it do a topological sort of the rules? Say we add a mandatory /and/ a deprecated rule about the same mixin. How does it handle dependency loops?

The answer is simple. It doesn't. The rules are applied once per mutation in the order in which they were added. It is the responsibility of the user to add them in some sensible order. Had the library provided some form of rule sort, it would have needlessly overcomplicated the custom rule definition, especially for cases in which you actually want to... well, overrule a rule.

So, that's all there is to know about mutation rules.

Multicast result combinators

(For the complete, working source of this example see combinators.cpp)

For this tutorial let's imagine we have a simple CAD program, which deals designing 3-dimensional objects. In such programs various aspects of an object need to be visible and editable at different times. Let's assume our objects are defined by their wireframe, their vertices, and their surface.

We'll define mixins for those and focus on the parts they have in common: namely whether an part of the object is visible, and how much elements is this part composed of.

class wireframe
{
public:
void set_visible(bool value);
void set_elements_count(int count);
bool visible() const;
int elements_count() const;
// ...
private:
bool _visible;
int _elements_count;
};
class vertices
{
public:
void set_visible(bool value);
void set_elements_count(int count);
bool visible() const;
int elements_count() const;
// ...
private:
bool _visible;
int _elements_count;
};
class surface
{
public:
void set_visible(bool value);
void set_elements_count(int count);
bool visible() const;
int elements_count() const;
// ...
private:
bool _visible;
int _elements_count;
};

Now let's define messages for the methods we'll want to access polymorphically and define our mixins to use those messages.

DYNAMIX_CONST_MULTICAST_MESSAGE_0(bool, visible);
DYNAMIX_CONST_MULTICAST_MESSAGE_0(int, elements_count);
DYNAMIX_DEFINE_MESSAGE(elements_count);
DYNAMIX_DEFINE_MIXIN(vertices, visible_msg & elements_count_msg);
DYNAMIX_DEFINE_MIXIN(wireframe, visible_msg & elements_count_msg);
DYNAMIX_DEFINE_MIXIN(surface, visible_msg & elements_count_msg);

As you can see those are multicast messages. Each of the mixins in the object will implement and respond to them.

Now let's create some objects.

const int NUM_OBJECTS = 20;
dynamix::object objects[NUM_OBJECTS];
const dynamix::object& o = objects[0];

...and mutate them with some mixins, and give them some values.

For example let's say all of our objects are cubes, that have 24 vertices (4 per side), 6 squares for the wireframe, and a single (folded) surface.

for(int i=0; i<NUM_OBJECTS; ++i)
{
dynamix::object& o = objects[i];
.add<vertices>()
.add<wireframe>()
.add<surface>();
o.get<vertices>()->set_elements_count(24);
o.get<vertices>()->set_visible(false);
o.get<wireframe>()->set_elements_count(6);
o.get<wireframe>()->set_visible(false);
o.get<surface>()->set_elements_count(1);
o.get<surface>()->set_visible(true);
}

As you may have noticed, all of our messages are functions that have a return value. You may have tried making multicast messages with non-void functions and noticed that the generated message function is void and doesn't return anything.

The things that will help us make use of the values returned from the messages are the multicast result combinators.

So, let's say we want to see if an object is visible. We say that it is visible if at least one of its mixins is. To get this value we may use the combinator boolean_or provided by the library (all built-in combinators are in namespace dynamix::combinators)

bool is_first_visible = visible<dynamix::combinators::boolean_or>(o);
cout << "The first object is " << (is_first_visible ? "visible" : "invisible") << "." << endl;

That's it. Giving a combinator as an explicit template argument to a multicast message call, will call a function that has a return value defined by the combinator. In this case boolean_or causes the message to return a bool which is true if at least one of the messages returns non-zero.

Had we defined that an object is visible if all of its mixins were visible, then we could have used the built-in combinator boolean_and.

Now let's look at another built-in combinator – sum. You may have guessed that it's a sum of all values returned by the messages. In our case we may want to check how many elements are there in an object:

cout << "There are " << elements_count<dynamix::combinators::sum>(o) << " elements in the first object." << endl;

All built-in combinators have an alternative usage. You saw the first, where putting the combinator as a template argument, causes the message to return a value.

The second usage keeps the message function void, but lets you add the combinator as an output parameter. This way, for example you may sum all elements throughout all objects with a single reusable combinator:

for(int i=0; i<NUM_OBJECTS; ++i)
{
elements_count(objects[i], sum);
}
cout << "There are " << sum.result() << " elements among all objects." << endl;

That's basically all there is to know about using combinators. Now, let's move on to creating our own custom ones. The built-in combinators are powerful, but sometimes you need to accomplish a task where you need some specific combinator behavior and need to add a custom one.

To create a custom combinator that's used as an output parameter is very easy. All you need to do is create a class, that has a public method called add_result. This public method should take one argument of the same type as (or one that can be implicitly cast to) the return type of the multicast message that you're "combining" with it. The method will be repeatedly called with each successful message with its return value as an argument. It should return booltrue when the execution should continue and false when it should stop.

We mentioned boolean_or and boolean_and. The function add_result in boolean_or returns false on the first non-zero value. That means it has determined the the final value is true (because at least one true has been met) and there is no need to execute the rest. Likewise boolean_and's add_result returns false on the first zero value it gets. Exactly as C++'s operators || and && behave.

So let's define our output parameter combinator that counts all mixins that have more than 1 element. Also, we could call it for a single object, but let's make use of the fact that it's an output parameter and count all mixins with more than 1 element among all objects:

struct more_than_1
{
more_than_1() : count(0) {}
bool add_result(int elements)
{
if(elements > 1)
++count;
// Never break. We need this for all mixins in an object
return true;
}
int count;
};
more_than_1 counter;
for(int i=0; i<NUM_OBJECTS; ++i)
{
elements_count(objects[i], counter);
}
cout << "There are " << counter.count << " mixins with more than 1 element among all objects." << endl;

Another case we need to cover is when you want your custom combinator to be added as a template argument to the message's function giving it a return value of its own.

To do this is only slightly more complicated the the previous return parameter case.

You need to create a template class whose template parameter will be provided by the message call and will be the message return type.

Next, as before you'll need an add_result method to be repeatedly called, again having a single argument of type equal to the message return type (you may just reuse the template argument of the combinator class), and again returning bool to indicate whether the message execution should continue or stop.

Next, you'll need a typedef result_type, which will indicate the return type of the message function.

Lastly, you'll have to create a method, called result, with no arguments, that has a return type result_type. It will be called when the execution is completed and it will provide the return value of the message function.

So, let's create an identical combinator as before – one that counts the mixins in an object that have more than one element, but this time to be used as a template argument of the message function.

template <typename MsgReturnType>
struct more_than_1_t
{
more_than_1_t() : count(0) {}
bool add_result(MsgReturnType elements)
{
if(elements > 1)
++count;
// Never break. We need this for all mixins in an object
return true;
}
// return type of message function
typedef int result_type;
result_type result() const
{
return count;
}
int count;
};

Now we can use our new custom combinator as we used boolean_or above.

cout << "There are " << elements_count<more_than_1_t>(o)
<< " mixins with more than 1 element in the first object." << endl;

The last example in this tutorial deals with finding the number of times your add_result function will be called.

Suppose you want to implement a combinator which collects all execution results of the messages in the multicast in a vector. Now, this is easy, given what we've learned so far. Just create the combinator and call vec.push_back for each result in add_result. Then simply use it like this:

auto results = elements_count<collection_t>(o);
cout << "The number of elemens per mixin in the first object is:\n";
for (auto i : results)
{
cout << '\t' << i << endl;
}

This, however, will potentially cause useless allocations. A much better implementation would call vec.reserve before calling push_back many times.

The library helps you do this by allowing combinator classes to have an optional method set_num_results(size_t). If a combinator has such, it will be called by the runtime before executing the methods associated with the multicast message. Here is a sample custom combinator which allows the user to collect all return values and also calls reserve with an appropriate size:

template <typename MsgReturnType>
struct collection_t
{
void set_num_results(size_t num)
{
cout << "collection_t reserving space for " << num << " results." << endl;
results.reserve(num);
}
bool add_result(MsgReturnType result)
{
results.push_back(result);
// Never break. We need this for all mixins in an object
return true;
}
// return type of message function
typedef std::vector<MsgReturnType> result_type;
result_type result() const
{
return results;
}
std::vector<MsgReturnType> results;
};

And that's all there is about multicast result combinators.

Tips and tricks

  • When adding the same set of messages to multiple mixins, create a define for them all. Like: #define transform_messages set_position_msg & set_orientation_msg. Then use it like this DYNAMIX_DEFINE_MIXIN(x, some_messages & transform_messages);
  • Instead of using the long message declaration macros, consider defining your own. Maybe something like #define C_MSG_1 DYNAMIX_CONST_MESSAGE_1
  • Prefer using object_type_template-s instead of mutating each new object in the same fashion.
  • Prefer using same_type_mutator when mutating multiple objects of the same type.
  • If you have some messages that are valid for all objects in your system, instead of adding them to a mixin present in every object, consider having some stand-alone functions where your first parameter is dynamix::object&. They will be indistinguishable from messages.
  • If you have multicast logic that needs to stop after a success in any of the message implementations in an object, have your messages return bool and then use the boolean_or combinator. It will stop the message execution on the first true.

Common features in objects

When developing software with DynaMix, you'll often find yourself needing some set of features added to every object. While adding the same mixin to all objects (be it manually or by a mutation rule) is an option, a much cleaner solution is to just use your own class for objects, which iherits from dynamix::object. For such a case consider adding your own versions of the dm_this and dynamix::object_of functions to return your own object type.

Subclassing dynamix::object instead of having a mixin common to all objects is the preferred way to accomplish shared object features. Apart from it being cleaner and easier to read, it's also a bit better in performance, because it will save you the indirections from getting to those features.

Try using a mixin common to all objects only it these cases:

  • When it contains functionality which can be overriden by other mixins (for such cases default message implementations is also something to consider).
  • When the functionality is not a core piece of the software but instead comes in an optional plug-in.

Here is an example solution:

class my_object : public dynamix::object
{
public:
// all objects in this software have id and name
std::string name;
int id;
// hide implementations from object to avoid slicing when copying
// copy your own data in these functions too
my_object copy() const;
void copy_from(const my_object& o);
void copy_matching_from(const my_object& o);
};
...
template <typename Mixin>
my_object* my_object_of(Mixin* m)
{
return static_cast<my_object*>(dynamix::object_of(m));
}
template <typename Mixin>
const my_object* my_object_of(const Mixin* m)
{
return static_cast<const my_object*>(dynamix::object_of(m));
}
#define my_bm_this my_object_of(this)
...
my_object o;
mutate(o)
.add<my_mixin>()
.add<other_mixin>();
...
void my_mixin::foo()
{
int own_object_id = my_bm_this->id;
...
}

Mixins with a common parent

Sometimes you will feel the need to have mixins with a common parent. Most likely this will happen when you want to define two different mixins that share some common functionality. Moving the shared functionality in the same common parent is a good idea and DynaMix will work exactly the same way if you do this. However there is a pitfall in this case. It happens when you have multiple inheritance. Due to the special nature in which the library arranges the memory internally, if a mixin type has more than one parent, using dm_this in some of those parents might lead to crashes.

More precisely, when the library allocates memory for a mixin type, it allocates a buffer that is slightly bigger than needed and puts the pointer to the owning object at its front. What dm_this does is actually an offset from this with the appropriate number of bytes for object*. So if a parent of your mixin type, other than the first, calls dm_this, it will end up returning an invalid pointer to the owning object.

To be able to have parents, other than the first, with access to the owning object we suggest that you create a pure virtual function that gets it from the actual mixin type.

Say virtual object* get_dm_object() = 0; in the parents, which is implemented in the child class (the actual mixin defined with DYNAMIX_DEFINE_MIXIN) by simply return dm_this.

Of course there are other ways to accomplish this, for example with CRTP, but the virtual function is probably the cleanest and safest one.

Common problems and solutions

Compiler errors

  • No overload of _dynamix_get_mixin_type_info supports the type
    • Problem: In your code you're referring to a mixin, that the library cannot recognize as such. This could be caused by a mutation, or by calling object::get or object::has.
    • Solution: Make sure you've spelled the name of the mixin correctly. Make sure the line that produces the error has visibility to the declaration of this mixin (by DYNAMIX_DECLARE_MIXIN)
  • No overload of _dynamix_get_mixin_feature supports the type
    • Problem: You're calling object::implements with a message that cannot be recognized as such.
    • Solution: Make sure you've spelled the message name correctly. Make sure you've added the _msg suffix to the message.
  • No overload of _dynamix_register_mixin_feature supports the type
  • ...or no overload of operator & supports the type
    • Problem: You're adding a mixin feature to the feature list in DYNAMIX_DEFINE_MIXIN that cannot be recognized as such
    • Solution: If it's a message, check that you've added the _msg suffix. If it's an allocator, make sure it's derived from the mixin_allocator class
  • Redefinition of _dynamix_get_mixin_feature
    • Problem: You're defining two messages with DYNAMIX_DEFINE_MESSAGE that have same name in the same file.
    • Solution: Declare the messages as overloads with DYNAMIX_xxx_MESSAGE_N_OVERLOAD. Define the overloads.

Linker errors

Undefined reference (mentioned below) will be reported as an "unresolved external symbol" in Visual C++.

  • Undefined reference to _dynamix_get_mixin_type_info
    • Problem 1: A type has been declared as a mixin, but hasn't been defined
    • Solution 1: Define the mixin with DYNAMIX_DEFINE_MIXIN
    • Problem 2: A mixin is being used from a dynamic library and hasn't been exported correctly.
    • Solution 2: Declare the mixin properly as DYNAMIX_DECLARE_EXPORTED_MIXIN
  • Undefined references to _dynamix_register_mixin_feature and _dynamix_get_mixin_feature
    • Problem 1: A message has been declared but not defined.
    • Solution 1: Define the message with DYNAMIX_DEFINE_MESSAGE
    • Problem 2 : A message is being used from a dynamic library and hasn't been exported correctly.
    • Solution 2: Declare the message properly as DYNAMIX_EXPORTED_xxx_MESSAGE_N
  • Multiple references to _dynamix_register_mixin_feature and _dynamix_get_mixin_feature
    • Problem: Two messages have been defined with the same name and different arguments.
    • Solution: Declare the messages as overloads with DYNAMIX_xxx_MESSAGE_N_OVERLOAD

Runtime errors

The exceptions the library may throw, what causes them, and how to fix them can be found in the reference. Besides them, these may also occur:

  • Assertion fails in domain.hpp: "you have to increase the maximum number of mixins"
    • Problem: The maximum number of registered mixins supported by the library has been surpassed.
    • Solution: You have to increase the value of DYNAMIX_MAX_MIXINS in the file config.hpp of the library and then rebuild it.
  • Assertion fails in domain.cpp: "you have to increase the maximum number of messages"
    • Problem: The maximum number of registered messages supported by the library has been surpassed.
    • Solution: You have to increase the value of DYNAMIX_MAX_MESSAGES in the file config.hpp of the library and then rebuild it.