DynaMix

A New Take on Polymorphism in C++

by Borislav Stanimirov

About

  • Hi. I'm Borislav.
  • DynaMix is a C++ library
  • History
    • 2007: Interface. Zahary Karadjov
    • 2013: Rebirth as Boost.Mixin. Me
    • 2016: Bye, Boost. Hello, DynaMix
  • Spread (to my knowledge)
    • One PC MMORPG
    • One released mobile game
    • Two mobile games in development

About 2.0

  • What will be in this talk?
    • Introducing the DynaMix library
    • We will focus on the "what" and the "why"
    • We will hardly even mention the "how"
  • So, what does this library do?
    • A means to create a project's architecture rather than achieve its purpose
    • Management of complex objects in potentially multiple subsystems
    • Enforces OOP practices like composition over inheritance, loose coupling, and separation of interface and implementation

The Gist

  • Building blocks
    • dynamix::object - just an empty object
    • Messages - function-like pieces of interface, that an object might implement
    • Mixins - classes that you've written which actually implement messages
  • Usage
    • Mutation - the process of adding and removing mixins from objects
    • Calling messages - like calling methods, this is where the actual business logic lies
  • I know what a mixin is and it's not this
    • DynaMix = dynamic mixins

A Basic Example

But why?

OOP and Polymorphism

  • OOP has come to imply dynamic polymorphism
    • Dynamic polymorphism is when the compiler can see a function call but can't know which actual piece of code will be executed next
    • It's in the category of things which are slower and can't have good compilation errors
    • Totally anti modern C++
  • OOP has been critized a lot
  • C++ is, among other things, an OOP language
  • Out of the box in an OOP context C++ only gives us virtual functions for polymorphism

An Example Problem


  • Suppose we want to have an object which is:
    • A flying creature
    • A two-legged creature
    • Is controlled by a hostile AI
    • Has an associated animated model
    • Has DirectX rendering code

That's easy


class dragon
{
public:
    void fly();
    void walk_on_two_legs();
    void ai_take_the_wheel();
    const model& get_model() const;
    void render_with_directx() const;
// ...
}

Implied Conditions


  • We will have other objects in our game
  • Different subsystems of the game care about different aspects of those objects
  • Different parts of the object may rely on others to work


What if we also have a horse or a human character?

Still easy


class dragon : public flying_creature,
    public two_legged_creature, public monster_ai,
    public animated_model, public directx_rendering
{};

class horse : public walking_creature,
    public four_legged_creature, public neutral_ai,
    public animated_model, public direcx_rendering
{};

// ... you get the point

That, in a way, is even worse, because flying_creature has no way of telling animated_model which animation to play.

OK. This time using real mixins


template <typename object_type>
class flying_creature
{
public:
    void fly()
    {
        flap_wings();
        static_cast<object_type*>(this)
            ->set_animation("flying");
    }
// ...
};

class dragon : public flying_creature<dragon>,
    public two_legged_creature<dragon>,
    public monster_ai<dragon>,
    public animated_model<dragon>,
    public directx_rendering<dragon>
{};



  • ALL of my code is in headers
  • How can I have an array of objects?



template <typename object_type>
class flying_creature : public virtual game_object
{ /* ... */ };

std::vector<game_object*> objects;

This is identical to vector<void*>.
I have no way of using the objects in this array.

Fine. I'm rolling-up my sleeves


class flying_creature : public virtual game_object
{
public:
    virtual void fly() override
    {
        this->flap_wings();
        this->set_animation("flying");
    }
// ...
};

class dragon : public flying_creature,
    public two_legged_creature, public monster_ai,
    public animated_model, public directx_rendering
{
};


So, all, ALL possible methods will exist as pure virtual in game_object

Also, a walking creature cannot fly. You can never instantiate dragon since it's abstract.



Having separate methods for flying and walking was a bad idea, anyway. There should be a single method: move. In fact how about this:


class game_object
{
    virtual void move() = 0; // flying, walking, vehicles
    // enemy/neutral ai, keyboard control
    virtual void decide_action() = 0;
    //...
};

class dragon : public flying_creature,
    public two_legged_creature, public monster_ai,
    public animated_model, public directx_rendering
{
    // there still might be invalid actions for the object
    // list them here
    virtual void use(item*) override {
        throw bad_call();
    }
};

This will work.

Aww, yiss!

But...


  • It's impractical
  • Every new type of object, needs to be explicity added to the code
  • Combinatorial explosion of types
  • game_object is a coupling focal point
  • As a result no software is written like this (except probably some beginner projects)

An Example Problem Cont.


  • Suppose that we also want
    • To sometimes manually control the dragon
    • To cut the wings off of the dragon and have a plain (well two-legged) lizzard
    • To optionally add fire-breathing powers to our dragon
    • To be able to choose the rendering API, say to switch to OpenGL


I could add such if-checks to all my methods, but I suppose you won't like this

Object


class game_object
{
    control* m_control;
    physical_data* m_physical_data;
    rendering* m_rendering;
    mobility* m_mobility;
    ...
};
// compose
game_entity dragon;
dragon.set_control(new monster_ai);
dragon.set_physical_data(new animated_model("dragon.x"));
dragon.set_mobility(new flyer);
...
// modify
dragon.set_control(new player_control);

Component


class component
{
    game_object* self;
};

class monster_ai
    : public control, public component
{
    virtual void decide_action() override
    {
        ...
        self->get_mobility()->move_to(good_guy);
    }
};

This is, actually, a pretty decent solution.


  • This is the interface to component pattern
  • There are games and CAD systems which use it
  • In fact (although not immediately obvious), using this as a base, you can recreate almost every feature of DynaMix (in a concrete and unreusable way)

But...

  • Every new type of interface needs to be explicitly added to the huge object class
  • More importantly: Interface classes are limiting

Pushing The Limits


struct mobility
{
    virtual void move_to(target t) = 0;
    virtual bool can_move_to(target t) const = 0;
};

What if we want to override only can_move_to?

OK. I give up. In such cases people just don't use C++. You want to embed a scripting language like Python, or Lua, or JavaScript.


That's a decent solution too.


module FlyingCreature
  def move_to(target)
      puts can_move_to?(target) ?
        "flying to #{target}"
        : "can't fly to #{target}"
  end
  def can_move_to?(target)
    true # flying creatures don't care
  end
end

module AfraidOfEvens
  def can_move_to?(target)
    return target%2 != 0
  end
end

a = Object.new
a.extend(FlyingCreature)
a.move_to(10)
a.extend(AfraidOfEvens)
a.move_to(10)

The Same in C++

  • Composition over inheritance
  • Late binding
  • "Messages" over "methods"
  • Messages separate from classes
  • Wait... Do we even want this in C++?
    • It has a better performance than scripting languages, even ones with JIT compilation
    • It's less complex since it lacks a binding layer
    • It lets you write more C++
    • It is compatible with scripting languages

DynaMix Solution

Finally

DynaMix Dragon


object dragon; // just an empty object

mutate(dragon)
    .add<flying_creature>()
    .add<two_legged_creature>()
    .add<monster_ai>()
    .add<animated_model>()
    .add<directx_rendering>();

::set_model(dragon, "dragon.x");
//...
::decide_action(dragon); // attack player

mutate(dragon)
    .remove<monster_ai>()
    .add<player_control>();

::decide_action(dragon); // read keyboard

DynaMix Mixin



class monster_ai
{
    void decide_action()
    {
        ...
        ::move_to(dm_this, good_guy);
    }
};
  • Yup. No inheritance. The libary is non-intrusive.
  • dm_this is like self: the object we're a part of

This Is Impossible!


  • Yes it is.
  • In a header file we need:
DYNAMIX_DECLARE_MIXIN(monster_ai);
  • This is enough to mutate objects
  • Also in a .cpp file we need:
DYNAMIX_DEFINE_MIXIN(monster_ai, decide_actions_msg);
  • This is how we "tell" the library which messages will be added to the object's interface, when this mixin is mutated-in

Messages



DYNAMIX_MESSAGE_0(void, decide_action);
DYNAMIX_MESSAGE_1(void, move_to, object*, target);
DYNAMIX_MESSAGE_2(rt, foo, arg1_t, arg1, arg2_t, arg2);

DYNAMIX_DEFINE_MESSAGE(decide_action);
DYNAMIX_DEFINE_MESSAGE(move_to);
DYNAMIX_DEFINE_MESSAGE(foo);

    And that's how we truly separate the interface from the implementation.

Eye candy time!

MixQuest

When to Use DynaMix?

  • When you're writing software with complex polymorphic objects
  • When you have subsystems which care about interface (rather than data)
  • When you want plugins which enable various aspects of your objects
  • Such types of projects include
    • Most CAD systems
    • Some games: especially RPGs and strategies
    • Some enterprise systems

When Not to Use DynaMix?


  • Small scale projects
  • Projects which have little use of polymorphism
  • Existing large projects
  • In performance critical code

Performance

  • Message calls, as any polymorphic call, are slower than function calls
  • They are comparable to std::function
  • Mutations can be fairly slow. Internal types
  • Memory overhead
    • For objects: pointers but mainly size of mixins
    • For unique types: sparse arrays of mixins
  • Thread safety
    • Calling messages is safe
    • Mutating an object in one thread and calling messages on it in another is not safe
    • Mutating two objects in two threads is safe

Recap

  • Compose and mutate objects from mixins
  • Have uni- and multicast messages
  • Manage message execution with priorities
  • Easily have hot-swappable or even releaseble plugins
  • There was no time for:
    • Custom allocators
    • Multicast result combinators
    • Implementation details

End

Questions?


Borislav Stanimirov / ibob.github.io


DynaMix is here: github.com/ibob/dynamix/
Link to these slides: http://ibob.github.io/slides/dynamix/
Slides license Creative Commons By 3.0
Creative Commons License