poly.md 5.6 KB

Crash Course: poly

Table of Contents

Introduction

Static polymorphism is a very powerful tool in C++, albeit sometimes cumbersome to obtain.
This module aims to make it simple and easy to use.

The library allows to define concepts as interfaces to fullfill with concrete classes withouth having to inherit from a common base.
This is, among others, one of the advantages of static polymorphism in general and of a generic wrapper like that offered by the poly class template in particular.
What users get is an object that can be passed around as such and not through a reference or a pointer, as happens when it comes to working with dynamic polymorphism.

Since the poly class template makes use of entt::any internally, it supports most of its features. Among the most important, the possibility to create aliases to existing objects and therefore not managed directly. This allows users to exploit the static polymorphism while maintaining ownership of their objects.
Likewise, the poly class template also benefits from the small buffer optimization offered by the entt::any class and therefore minimizes the number of allocations, avoiding them altogether where possible.

Other libraries

There are some very interesting libraries regarding static polymorphism.
Among all, the two that I prefer are:

  • dyno: runtime polymorphism done right.
  • Poly: a class template that makes it easy to define a type-erasing polymorphic object wrapper.

The former is admittedly an experimental library, with many interesting ideas. I've some doubts about the usefulness of some features in real world projects, but perhaps my ignorance comes into play here. In my opinion, its only flaw is the API which I find slightly more cumbersome than other solutions.
The latter was undoubtedly a source of inspiration for this module, although I opted for different choices in the implementation of both the final API and some features.

Either way, the authors are gurus of the C++ community, people I only have to learn from.

Concept and implementation

The first thing to do to create a type-erasing polymorphic object wrapper (to use the terminology introduced by Eric Niebler) is to define a concept that types will have to adhere to.
In EnTT, this translates into the definition of a template class as follows:

template<typename Base>
struct Drawable: Base {
    void draw() { this->template invoke<0>(*this); }
};

The example is purposely minimal but the functions can receive values and return arguments. The former will be returned by the call to invoke, the latter must be passed to the same function after the reference to this instead.
As for invoke, this is a name that is injected into the concept through Base, from which one must necessarily inherit. Since it's also a dependent name, the this-> template form is unfortunately necessary due to the rules of the language. However, there exists also an alternative that goes through an external call:

template<typename Base>
struct Drawable: Base {
    void draw() { entt::poly_call<0>(*this); }
};

Once the concept is defined, users need to specialize a template variable to tell the system how any type can satisfy its requirements:

template<typename Type>
inline constexpr auto entt::poly_impl<Drawable, Type> = entt::value_list<&Type::draw>{};

In this case, it's stated that the draw method of a generic type will be enough to satisfy the requirements of the Drawable concept.
The poly_impl variable template can be specialized in a generic way as in the example above, or for a specific type where this satisfies the requirements differently. Moreover, it's easy to specialize it for families of types:

template<typename Type>
inline constexpr auto entt::poly_impl<Drawable, std::vector<Type>> = entt::value_list<&std::vector<Type>::size>{};

Finally, an implementation doesn't have to consist of just member functions. Free functions are an alternative to fill any gaps in the interface of a type:

template<typename Type>
void print(Type &self) { self.print(); }

template<typename Type>
inline constexpr auto entt::poly_impl<Drawable, Type> = entt::value_list<&print<Type>>{};

Refer to the variable template definition for more details.

Static polymorphism in the wild

Once the concept and implementation have been introduced, it will be possible to use the poly class template to contain instances that meet the requirements:

using drawable = entt::poly<Drawable>;

struct circle {
    void draw() { /* ... */ }
};

struct square {
    void draw() { /* ... */ }
};

// ...

drawable d{circle{}};
d.draw();

d = square{};
d.draw();

The poly class template offers a wide range of constructors, from the default one (which will return an uninitialized poly object) to the copy and move constructor, as well as the ability to create objects in-place.
Among others, there is a constructor that allows users to wrap unmanaged objects in a poly instance:

circle c;
drawable d{std::ref(c)};

In this case, although the interface of the poly object doesn't change, it won't construct any element or take care of destroying the referenced object.