DynaMix  1.3.7
A new take on polymorphism in C++
Introduction
logo.png

Abstract

DynaMix is a library which allows the composition and modification of polymorphic objects at run time. Objects are constructed from building blocks called "mixins" enabling an effect similar to multiple inheritance, while allowing the client code to remain oblivious to the actual composition of the objects.

A take on the Composition over inheritance technique, the result resembles the mixins in Ruby. It can also be compared to the inheritance in Eiffel or the traits in Self, Scala, PHP, and many others, or the roles in Perl.

This is given while also having full abstraction between the interface and the definition of types – a problem often given as the motivation for the PIMPL idiom.

In short, DynaMix is an alternative way to accomplish polymorphism. It is a means to create a project's architecture, rather than implement its purpose.

The library uses the type dynamix::object as a placeholder, whose instances can be extended with existing classes (mixins), thus providing a particular instance with the functionality of all those types. Accessing the newly formed type's interface is made through messages – stand-alone functions generated by the library, which can be thought of as methods.

DynaMix focuses on maximal performance and minimal memory overhead.

Other meanings of "mixin" in C++

The term "Mixin" is not to be confused with another meaning, popular in C++, namely CRTP mixins. This particular use of CRTP in this document shall henceforth be referred to as "traits", as the exact same functionality is called "traits" in many other languages.

Other meanings of "mixin" in other languages

In D the term mixin exists and mixin is a keyword. It is a compile time feature, that has some similarities with the macros in C and C++, and none at all with the mixins in DynaMix. It is a completely different concept.

Basic look and feel of the code with DynaMix

Here is a very small and incomplete example of what your code may look like if you use the library:

// assuming my_objects.get_ally(0); is a way to get an ally to the
// main character in a game
dynamix::object& obj = my_objects.get_ally(0);
// now let's make the object think some positive thoughts about the
// main character
think(obj); // C++ doesn't allow us to have obj.think().
// DynaMix's messages are standalone functions
// object composition
.add<flying_creature>();
// object can now respond to fly()
fly(obj); // ...instead of obj.fly()
// object mutation
.remove<ally>()
.add<enemy>();
think(obj); // the same object now thinks negative thoughts about the main
// character, since it's no longer an ally, but an enemy

For a more detailed, working example see the basic usage tutorial.

List of features

  • Compose objects from mixins at run time
  • Physically separate interface and implementation
  • Non-intrusive – mixins don't need to have a common parent or any special code inside
  • No external dependencies other than the standard library
  • Fast polymorphic calls – comparable to std::function
  • Mutate "live" objects by changing their composition at run time
  • Have multicast messages, which are handled by many mixins within an object
  • Possibility to have custom allocators to finely tune the memory and aim for cache-locality for critical parts of the code
  • Ability to have dynamic libraries that can enrich or modify objects, without modifying (or even rebuilding) the executable.
  • Thread safe message calls – as thread safe as the underlying methods.

When (and when not) to use DynaMix

The more complex your objects are, the more beneficial it will be to use the library. Pieces of software that typically have very complex objects include games (especially role-playing ones or strategies), CAD systems, enterprise systems, UI libraries, and more.

As a general rule of thumb: if you have complex polymorphic objects, DynaMix is a good choice.

We should emphasize on the polymorphism. In many very high-performance systems polymorphism is avoided at the cost of code that is (at least somewhat) harder to write and maintain (this is most often the case with high-end games). Since such systems will try to "squeeze" every possible piece of processing power out of the CPU, cache locality and lack of cache misses are critical in some parts of their code. As is the case with all instances of polymorphism, including C++ virtual methods and std::function, uses of DynaMix's features will almost certainly lead to cache misses. Of course, you may still rely on the library in other parts of your code, like the business (or gameplay) logic. For more information about the library performance, see the Performance section.

Of course, small projects with simple objects, even if they are polymorphic, may end up not finding any particular benefits in using the library, since their size makes them fast to compile and easy to maintain as they are. If a piece of software can be created in a couple of days, by one or two programmers, there will hardly be any need for DynaMix.

Comparisons

Comparison with multiple inheritance

The closest thing to DynaMix that C++ can offer out of the box is multiple inheritance. A dynamix::object composed of some mixins, can be thought of as an instance of an empty class that's derived from these classes – the object's interface will be equal to the union of the interfaces of its mixins, and it will internally instantiate them.

Here's a comprehensive list of the most important differences between DynaMix and C++ multiple inheritance:

  • No combinatorial explosion of types: you compose types at runtime, and don't need to explicitly list all possible combinations of building-block types.
  • No type-bound interfaces: since the interface is physically separated from the implementation (or type), you don't need to have a single class implement an interface. You could separate it between multiple classes or combine interfaces in a single class.
  • Live object mutation: change the interface and/or implementation of messages in an object at runtime.
  • Natural common reference: in order to have containers of your objects composed by multiple inheritance, you need to define a "master type" from which all parents need to be derived by virtual inheritance. Such a type would be a coupling "focal point" and would need to be frequently changed while you develop your software. In DynaMix this is a no-issue, since all objects in the library are of type dynamix::object.

However, even-though compared to other libraries that have similar features, DynaMix is one of the fastest and with the least memory overhead, using the library comes with some inevitable downsides when compared with plain multiple inheritance:

  • No compile-time type: An object derived from, say, foo and bar can implicitly be cast to foo or bar. Since the library uses a placeholder type – dynamix::object – implicit casts to any of its mixins are impossible. That aside, the hypothetical object from above will receive all of the methods from foo and bar, but while this is true for the dynamix::object with such mixins, a compilation error cannot be generated if a message is called that the object can't handle. A runtime error will be generated instead, which is the norm in similar libraries, but is harder to catch and debug than it is with plain multiple inheritance.
  • Memory overhead per type: Each unique combination of mixins is internally represented by a type, which has type metadata – for example an internal alternative to a virtual table. The type metadata can take up to two kilobytes of memory. Unless a type is instantiated its metadata won't be created, but still if you expect to have a huge variety of types, you may want to keep this in mind. Of course systems that are expected to have thousands of different object types, usually wouldn't care about a couple of extra megabytes of memory, but it could happen.
  • Memory overhead per object: An instance of an empty class that has multiple parents takes up memory equal to the sizes of all of its parents. This is inevitable. A dynamix::objects naturally takes up the same amount plus an additional pointer for the type metadata, plus N pointers used for the special dm_this pointer, where N is equal to the number of mixins within. So, in short, the memory overhead of an object composed on N mixins, is N+1 times sizeof(intptr_t) ((N+1) * 8 bytes on a 64 bit system).
  • Speed of message calls: A message call is slower than a regular non-virtual method call. Exactly how much is very hard to estimate, since this depends very much on cache locality, but a message call can easily be compared to a std::function call in terms of speed, which in most (but not all) cases is a negligible overhead.

DynaMix as an entity-component-system

If you're familiar with entity-component-systems, one way of looking at at the library is as if it is one of those, and indeed, it has many features that are characteristic for such systems. More specifically the Interface to Component pattern

If you're not familiar with entity-component-systems, you might want to check out the appendix entry on them.

Here's how DynaMix is like an Interface to Component ECS:

  • dynamix::object can be interpreted as an entity in an ECS. It's just an empty class, that needs to be "built" from mixins.
  • consequently mixins can be thought of as components. You use mixins to build objects just as you use components to build entities.
  • As with any ECS, you can mutate objects by adding/removing/substituting mixins.

However, DynaMix is not strictly an ECS. Here's a list of the differences.

  • It is non-intrusive: Mixins don't need to inherit from a common parent, nor do you need to change the code of a class to "turn it into" a mixin.
  • Instead of classes with pure virtual methods, messages are used to represent the functional interface of mixins, and through that – of objects.
  • Because of the previous two differences, dynamix::object-s can be (and are) completely oblivious to what kinds of mixins there may be, allowing you to truly, physically separate a program's subsystems. The entity in an ECS on the other hand, usually has at least some knowledge of all the possible component types (like the top level parent classes, for example).
  • Some entity-component-systems allow several entities to point to the same instance of a component. This is not possible in DynaMix. A mixin instance is bound to an object instance
  • Some entity-component-systems allow an entity to contain several instances of the same component type. This too is not possible in DynaMix. An object can have either zero or one instances of a given mixin type.

Note that some data structures, sometimes called entity-component-systems, are not used to create and manage polymorphic objects, but instead are used to bind a strictly predefined set of concrete components to the same entity for a very performance intensive piece of software. The components are used by different subsystems, that require them to be aligned in dense arrays for faster processing, without cache misses. As we mentioned before, DynaMix isn't designed for such cases, and while in terms of design and ease of use, it is a better choice than such an approach, it cannot help you in their main goal. Still, a non-polymorphic ECS and DynaMix can be (and have been) used together in a signle piece of software where DynaMix is used in the business logic subsystem, while the performance critical low-level parts of the software make use of a non-polymorphic ECS.

Comparison with COM

There are indeed similarities between DynaMix and Microsoft Component Object Model. DynaMix object are composed out of mixins much like COM's objects are composed from components. Indeed many patterns in DynaMix might be familiar to programmers familiar with COM. The notable differences are:

  • DynaMix has multicasts
  • With DynaMix there are no interfaces per se. The messages are standalone functions and a mixin implements an arbitrary combination of them.
  • Removing a mixin from an object in restores the object's previous state
  • DynaMix objects feel more like idiomatic C++ objects: They can be moved and copied and they have destructors instead of relying on a Release method.

Comparison with traits

As mentioned above DynaMix is similar to a feature of many other languages, called "traits". The exact same feature can be mimicked in C++ with CRTP. You may have heard the term "mixin" being mentioned in a C++ context before. It is very likely that what was meant was this CRTP style of creating types from existing ones.

Indeed, at least superficially, both DynaMix and said traits are very similar as both are used to create new types by combining existing ones, while also solving one of the problems of plain old multiple inheritance - the communication between the different components that comprise the object.

However much of the multiple inheritance problems remain:

  • Types composed of traits are static. Most languages (including C++ using the aforementioned mimic) don't allow you to change an object's type.
  • There is still a combinatorial explosion of types: each possible combination needs to be explicitly coded.
  • In strongly typed languages, like C++, there is still the problem of having a common interface to all objects.
  • In C++ you are bound to having the same compilation dependencies because of the header files that need to be included to compose types.

DynaMix solves all of these problems.

Comparison with Ruby mixins

If you're familiar with the mixins in Ruby, perhaps you will find a lot of similarities between them and DynaMix.

Ruby's mixins can of course be used just like traits, however extending existing objects with modules is also allowed via Object::extend. This is almost exactly what DynaMix allows you to do. Indeed, Ruby has been a great inspiration for this library.

Still, barring the differences that arise from C++ being a strongly typed language, for a small amount of extra code DynaMix allows you to do much more:

  • Removing mixins: in Ruby, when you extend an object with a module, all functions that both object and the module have are permanently overridden in the object by their versions from the module. DynaMix allows you to temporarily mutate the object and then remove the mixin while restoring the object exactly to its former self.
  • Prioritize messages: in DynaMix you can set priorities to the messages and thus when extending an object with a mixin, if a message is implemented by the object with a higher priority, even though it is also supplied by the mixin, in won't override the one already in the object.
  • Multicast messages: in Ruby (as in almost every other language with a similar functionality) a method call leads to a piece of code, defined for that method. DynaMix allows you to have messages that are handled by more than one of mixins in an object.