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.
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.
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.
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.