DynaMix
1.3.7
A new take on polymorphism in C++
|
(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.
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.
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.
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)
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:
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)
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:
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:
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:
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.
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)
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.
Both calls to trace
from above will display info about the newly constructed objects in the console.
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.
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.
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:
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_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:
And that concludes our tutorial on messages.
(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:
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:
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.
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).
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:
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:
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:
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:
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.
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:
(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:
We won't concern ourselves with their concrete functionality, so we'll just leave them with no messages.
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.
...and then change it. Let's assume we're switching our rendering platform.
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.
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:
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:
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
.
Now the mutation is not performed, and its state is empty.
You may safely apply empty mutations to an object:
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:
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.
Now obj2
has the mixins game_object
and directx_rendering
.
Let's create a new type template.
...to illustrate the other way of applying it to an object:
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:
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.
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.
(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.
We also have mixins that describe the frame of the piece of furniture.
Let's also define some mixins that will be responsible for the object serialization.
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.
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.
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
.
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.
That's it. Now all we have to do is add our mutation rule and it will be active.
After this mutation our custom mutation rule has also added container
to the object.
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.
(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.
Now let's define messages for the methods we'll want to access polymorphically and define our mixins to use those messages.
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.
...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.
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
)
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:
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:
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 bool
– true
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:
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.
Now we can use our new custom combinator as we used boolean_or
above.
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:
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:
And that's all there is about multicast result combinators.
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);
#define C_MSG_1 DYNAMIX_CONST_MESSAGE_1
object_type_template
-s instead of mutating each new object in the same fashion.same_type_mutator
when mutating multiple objects of the same type.dynamix::object&
. They will be indistinguishable from messages.bool
and then use the boolean_or
combinator. It will stop the message execution on the first true
.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:
Here is an example solution:
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.
_dynamix_get_mixin_type_info
supports the typeobject::get
or object::has
.DYNAMIX_DECLARE_MIXIN
)_dynamix_get_mixin_feature
supports the typeobject::implements
with a message that cannot be recognized as such._msg
suffix to the message._dynamix_register_mixin_feature
supports the type&
supports the typeDYNAMIX_DEFINE_MIXIN
that cannot be recognized as such_msg
suffix. If it's an allocator, make sure it's derived from the mixin_allocator
classDYNAMIX_DEFINE_MESSAGE
that have same name in the same file.DYNAMIX_xxx_MESSAGE_N_OVERLOAD
. Define the overloads.Undefined reference (mentioned below) will be reported as an "unresolved external symbol" in Visual C++.
_dynamix_get_mixin_type_info
DYNAMIX_DEFINE_MIXIN
DYNAMIX_DECLARE_EXPORTED_MIXIN
_dynamix_register_mixin_feature
and _dynamix_get_mixin_feature
DYNAMIX_DEFINE_MESSAGE
DYNAMIX_EXPORTED_xxx_MESSAGE_N
_dynamix_register_mixin_feature
and _dynamix_get_mixin_feature
DYNAMIX_xxx_MESSAGE_N_OVERLOAD
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:
domain.hpp
: "you have to increase the maximum number of
mixins"DYNAMIX_MAX_MIXINS
in the file config.hpp
of the library and then rebuild it.domain.cpp
: "you have to increase the maximum number of
messages"DYNAMIX_MAX_MESSAGES
in the file config.hpp
of the library and then rebuild it.