poly: first implementation

This commit is contained in:
Michele Caini
2020-12-02 15:49:40 +01:00
parent b1369582e1
commit 2f52bef882
6 changed files with 646 additions and 2 deletions

View File

@@ -87,10 +87,11 @@ Here is a brief, yet incomplete list of what it offers today:
* Views and groups to iterate entities and components and allow different access
patterns, from **perfect SoA** to fully random.
* A lot of **facilities** built on top of the entity-component system to help
the users and avoid reinventing the wheel (dependencies, snapshot, actor
class, support for **reactive systems** and so on).
the users and avoid reinventing the wheel (dependencies, snapshot, handles,
support for **reactive systems** and so on).
* The smallest and most basic implementation of a **service locator** ever seen.
* A built-in, non-intrusive and macro-free runtime **reflection system**.
* **Static polymorphism** made simple and within everyone's reach.
* A **cooperative scheduler** for processes of any type.
* All that is needed for **resource management** (cache, loaders, handles).
* Delegates, **signal handlers** (with built-in support for collectors) and a

161
docs/md/poly.md Normal file
View File

@@ -0,0 +1,161 @@
# Crash Course: poly
<!--
@cond TURN_OFF_DOXYGEN
-->
# Table of Contents
* [Introduction](#introduction)
* [Other libraries](#other-libraries)
* [Concept and implementation](#concept-and-implementation)
* [Static polymorphism in the wild](#static-polymorphism-in-the-wild)
<!--
@endcond TURN_OFF_DOXYGEN
-->
# Introduction
Static polymorphism is a very powerful tool in C++, albeit sometimes cumbersome
to obtain.<br/>
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.<br/>
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.<br/>
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.<br/>
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.<br/>
Among all, the two that I prefer are:
* [`dyno`](https://github.com/ldionne/dyno): runtime polymorphism done right.
* [`Poly`](https://github.com/facebook/folly/blob/master/folly/docs/Poly.md):
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.<br/>
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.<br/>
In `EnTT`, this translates into the definition of a template class as follows:
```cpp
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.<br/>
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:
```cpp
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:
```cpp
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.<br/>
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:
```cpp
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:
```cpp
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:
```cpp
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.<br/>
Among others, there is a constructor that allows users to wrap unmanaged objects
in a `poly` instance:
```cpp
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.

View File

@@ -35,6 +35,7 @@
#include "meta/resolve.hpp"
#include "meta/type_traits.hpp"
#include "platform/android-ndk-r17.hpp"
#include "poly/poly.hpp"
#include "process/process.hpp"
#include "process/scheduler.hpp"
#include "resource/cache.hpp"

329
src/entt/poly/poly.hpp Normal file
View File

@@ -0,0 +1,329 @@
#ifndef ENTT_POLY_POLY_HPP
#define ENTT_POLY_POLY_HPP
#include <cstddef>
#include <functional>
#include <tuple>
#include <type_traits>
#include <utility>
#include "../core/any.hpp"
#include "../core/type_info.hpp"
#include "../core/type_traits.hpp"
namespace entt {
/**
* @brief Inline variable designed to contain the definition of a concept.
* @tparam Concept A concept class template.
* @tparam Type The type for which the definition is provided.
*/
template<template<typename> class Concept, typename Type>
inline constexpr auto poly_impl = value_list{};
/*! @brief Static virtual table factory. */
class poly_vtable {
template<typename Type, auto Candidate, typename Ret, typename... Args>
[[nodiscard]] static auto * vtable_entry(Ret(*)(Type &, Args...)) {
return +[](any &any, Args... args) -> Ret {
return std::invoke(Candidate, any_cast<Type &>(any), std::forward<Args>(args)...);
};
}
template<typename Type, auto Candidate, typename Ret, typename... Args>
[[nodiscard]] static auto * vtable_entry(Ret(*)(const Type &, Args...)) {
return +[](const any &any, Args... args) -> Ret {
return std::invoke(Candidate, any_cast<std::add_const_t<Type> &>(any), std::forward<Args>(args)...);
};
}
template<typename Type, auto Candidate, typename Ret, typename... Args>
[[nodiscard]] static auto * vtable_entry(Ret(Type:: *)(Args...)) {
return +[](any &any, Args... args) -> Ret {
return std::invoke(Candidate, any_cast<Type &>(any), std::forward<Args>(args)...);
};
}
template<typename Type, auto Candidate, typename Ret, typename... Args>
[[nodiscard]] static auto * vtable_entry(Ret(Type:: *)(Args...) const) {
return +[](const any &any, Args... args) -> Ret {
return std::invoke(Candidate, any_cast<std::add_const_t<Type> &>(any), std::forward<Args>(args)...);
};
}
template<typename Type, auto... Impl>
[[nodiscard]] static auto * instance(value_list<Impl...>) {
static const auto vtable = std::make_tuple(vtable_entry<Type, Impl>(Impl)...);
return &vtable;
}
public:
/**
* @brief Returns a static virtual table for a specific concept and type.
* @tparam Concept A concept class template.
* @tparam Type The type for which to generate the virtual table.
* @return A static virtual table for the given concept and type.
*/
template<template<typename> class Concept, typename Type>
[[nodiscard]] static auto * instance() {
return instance<Type>(poly_impl<Concept, Type>);
}
};
/*! @brief Inspector class used to infer the type of the virtual table. */
struct poly_inspector {
/**
* @brief Generic conversion operator (definition only).
* @tparam Type Type to which conversion is requested.
*/
template <class Type>
operator Type &&() const;
/**
* @brief Dummy invocation function (definition only).
* @tparam Member Index of the function to invoke.
* @tparam Args Types of arguments to pass to the function.
* @param args The arguments to pass to the function.
* @return A poly inspector convertible to any type.
*/
template<auto Member, typename... Args>
poly_inspector invoke(Args &&... args) const;
/*! @copydoc invoke */
template<auto Member, typename... Args>
poly_inspector invoke(Args &&... args);
};
/**
* @brief Poly base class used to inject functionalities into concepts.
* @tparam Poly The outermost poly class.
*/
template<typename Poly>
struct poly_base {
/**
* @brief Invokes a function from the static virtual table.
* @tparam Member Index of the function to invoke.
* @tparam Args Types of arguments to pass to the function.
* @param self A reference to the poly object that made the call.
* @param args The arguments to pass to the function.
* @return The return value of the invoked function, if any.
*/
template<auto Member, typename... Args>
[[nodiscard]] decltype(auto) invoke(const poly_base &self, Args &&... args) const {
const auto &poly = static_cast<const Poly &>(self);
return std::get<Member>(*poly.vtable)(poly.storage, std::forward<Args>(args)...);
}
/*! @copydoc invoke */
template<auto Member, typename... Args>
[[nodiscard]] decltype(auto) invoke(poly_base &self, Args &&... args) {
auto &poly = static_cast<Poly &>(self);
return std::get<Member>(*poly.vtable)(poly.storage, std::forward<Args>(args)...);
}
};
/**
* @brief Shortcut for calling `poly_base<Type>::invoke`.
* @tparam Member Index of the function to invoke.
* @tparam Poly A fully defined poly object.
* @tparam Args Types of arguments to pass to the function.
* @param self A reference to the poly object that made the call.
* @param args The arguments to pass to the function.
* @return The return value of the invoked function, if any.
*/
template<auto Member, typename Poly, typename... Args>
decltype(auto) poly_call(Poly &&self, Args &&... args) {
return std::forward<Poly>(self).template invoke<Member>(self, std::forward<Args>(args)...);
}
/**
* @brief Static polymorphism made simple and within everyone's reach.
*
* Static polymorphism is a very powerful tool in C++, albeit sometimes
* cumbersome to obtain.<br/>
* This class aims to make it simple and easy to use.
*
* Below is a minimal example of use:
*
* ```cpp
* template<typename Base>
* struct Drawable: Base {
* void draw() { entt::poly_call<0>(*this); }
* };
*
* template<typename Type>
* inline constexpr auto entt::poly_impl<Drawable, Type> = entt::value_list<&Type::draw>{};
*
* using drawable = entt::poly<Drawable>;
*
* struct circle { void draw() {} };
* struct square { void draw() {} };
*
* int main() {
* drawable d{circle{}};
* d.draw();
*
* d = square{};
* d.draw();
* }
* ```
*
* The `poly` class template also supports aliasing for unmanaged objects.
* Moreover, thanks to small buffer optimization, it limits the number of
* allocations to a minimum where possible.
*
* @tparam Concept Concept class template.
*/
template<template<typename> class Concept>
class poly: public Concept<poly_base<poly<Concept>>> {
/*! @brief A poly base is allowed to snoop into a poly object. */
friend struct poly_base<poly<Concept>>;
using vtable_t = std::remove_pointer_t<decltype(poly_vtable::instance<Concept, Concept<poly_inspector>>())>;
public:
/*! @brief Default constructor. */
poly() ENTT_NOEXCEPT
: storage{},
vtable{}
{}
/**
* @brief Constructs a poly by directly initializing the new object.
* @tparam Type Type of object to use to initialize the poly.
* @tparam Args Types of arguments to use to construct the new instance.
* @param args Parameters to use to construct the instance.
*/
template<typename Type, typename... Args>
explicit poly(std::in_place_type_t<Type>, Args &&... args)
: storage{std::in_place_type<Type>, std::forward<Args>(args)...},
vtable{poly_vtable::instance<Concept, Type>()}
{}
/**
* @brief Constructs a poly that holds an unmanaged object.
* @tparam Type Type of object to use to initialize the poly.
* @param value An instance of an object to use to initialize the poly.
*/
template<typename Type>
poly(std::reference_wrapper<Type> value)
: storage{value},
vtable{poly_vtable::instance<Concept, Type>()}
{}
/**
* @brief Constructs a poly from a given value.
* @tparam Type Type of object to use to initialize the poly.
* @param value An instance of an object to use to initialize the poly.
*/
template<typename Type, typename = std::enable_if_t<!std::is_same_v<std::remove_cv_t<std::remove_reference_t<Type>>, poly>>>
poly(Type &&value)
: poly{std::in_place_type<std::remove_cv_t<std::remove_reference_t<Type>>>, std::forward<Type>(value)}
{}
/**
* @brief Copy constructor.
* @param other The instance to copy from.
*/
poly(const poly &other) = default;
/**
* @brief Move constructor.
* @param other The instance to move from.
*/
poly(poly &&other)
: poly{}
{
swap(*this, other);
}
/**
* @brief Assignment operator.
* @param other The instance to assign from.
* @return This poly object.
*/
poly & operator=(poly other) {
swap(other, *this);
return *this;
}
/**
* @brief Returns the type of the contained object.
* @return The type of the contained object, if any.
*/
[[nodiscard]] type_info type() const ENTT_NOEXCEPT {
return storage.type();
}
/**
* @brief Returns an opaque pointer to the contained instance.
* @return An opaque pointer the contained instance, if any.
*/
[[nodiscard]] const void * data() const ENTT_NOEXCEPT {
return storage.data();
}
/*! @copydoc data */
[[nodiscard]] void * data() ENTT_NOEXCEPT {
return const_cast<void *>(std::as_const(*this).data());
}
/**
* @brief Replaces the contained object by creating a new instance directly.
* @tparam Type Type of object to use to initialize the poly.
* @tparam Args Types of arguments to use to construct the new instance.
* @param args Parameters to use to construct the instance.
*/
template<typename Type, typename... Args>
void emplace(Args &&... args) {
storage.emplace<Type>(std::forward<Args>(args)...);
vtable = poly_vtable::instance<Concept, Type>();
}
/**
* @brief Aliasing constructor.
* @return A poly that shares a reference to an unmanaged object.
*/
[[nodiscard]] poly ref() const ENTT_NOEXCEPT {
poly other{};
other.storage = storage.ref();
other.vtable = vtable;
return other;
}
/**
* @brief Returns false if a poly is empty, true otherwise.
* @return False if the poly is empty, true otherwise.
*/
[[nodiscard]] explicit operator bool() const ENTT_NOEXCEPT {
return !(vtable == nullptr);
}
/**
* @brief Swaps two poly objects.
* @param lhs A valid poly object.
* @param rhs A valid poly object.
*/
friend void swap(poly &lhs, poly &rhs) {
using std::swap;
swap(lhs.storage, rhs.storage);
swap(lhs.vtable, rhs.vtable);
}
private:
any storage;
const vtable_t *vtable;
};
}
#endif

View File

@@ -197,6 +197,10 @@ SETUP_BASIC_TEST(meta_prop entt/meta/meta_prop.cpp)
SETUP_BASIC_TEST(meta_range entt/meta/meta_range.cpp)
SETUP_BASIC_TEST(meta_type entt/meta/meta_type.cpp)
# Test poly
SETUP_BASIC_TEST(poly entt/poly/poly.cpp)
# Test process
SETUP_BASIC_TEST(process entt/process/process.cpp)

148
test/entt/poly/poly.cpp Normal file
View File

@@ -0,0 +1,148 @@
#include <functional>
#include <type_traits>
#include <gtest/gtest.h>
#include <entt/poly/poly.hpp>
template<typename Base>
struct concept: Base {
void incr() { entt::poly_call<0>(*this); }
void set(int v) { entt::poly_call<1>(*this, v); }
int get() const { return entt::poly_call<2>(*this); }
void decr() { entt::poly_call<3>(*this); }
int mul(int v) { return entt::poly_call<4>(*this, v); }
};
template<typename Type>
void decr(Type &self) {
self.set(self.get()-1);
}
template<typename Type>
int mul(const Type &self, int v) {
return v * self.get();
}
template<typename Type>
inline constexpr auto entt::poly_impl<concept, Type> =
entt::value_list<
&Type::incr,
&Type::set,
&Type::get,
&decr<Type>,
&mul<Type>
>{};
struct impl {
void incr() { ++value; }
void set(int v) { value = v; }
int get() const { return value; }
int value{};
};
TEST(Poly, Functionalities) {
impl instance{};
entt::poly<concept> empty{};
entt::poly<concept> in_place{std::in_place_type<impl>, 3};
entt::poly<concept> alias{std::ref(instance)};
entt::poly<concept> value{impl{}};
ASSERT_FALSE(empty);
ASSERT_TRUE(in_place);
ASSERT_TRUE(alias);
ASSERT_TRUE(value);
ASSERT_EQ(empty.type(), entt::type_info{});
ASSERT_EQ(in_place.type(), entt::type_id<impl>());
ASSERT_EQ(alias.type(), entt::type_id<impl>());
ASSERT_EQ(value.type(), entt::type_id<impl>());
ASSERT_EQ(alias.data(), &instance);
ASSERT_EQ(std::as_const(alias).data(), &instance);
empty = impl{};
ASSERT_TRUE(empty);
ASSERT_NE(empty.data(), nullptr);
ASSERT_NE(std::as_const(empty).data(), nullptr);
ASSERT_EQ(empty.type(), entt::type_id<impl>());
ASSERT_EQ(empty.get(), 0);
empty.emplace<impl>(3);
ASSERT_TRUE(empty);
ASSERT_EQ(empty.get(), 3);
entt::poly<concept> ref = in_place.ref();
ASSERT_TRUE(ref);
ASSERT_NE(ref.data(), nullptr);
ASSERT_EQ(ref.data(), in_place.data());
ASSERT_EQ(std::as_const(ref).data(), std::as_const(in_place).data());
ASSERT_EQ(ref.type(), entt::type_id<impl>());
ASSERT_EQ(ref.get(), 3);
entt::poly<concept> null{};
std::swap(empty, null);
ASSERT_FALSE(empty);
entt::poly<concept> copy = in_place;
ASSERT_TRUE(copy);
ASSERT_EQ(copy.get(), 3);
entt::poly<concept> move = std::move(copy);
ASSERT_TRUE(move);
ASSERT_FALSE(copy);
ASSERT_EQ(move.get(), 3);
}
TEST(Poly, Owned) {
entt::poly<concept> poly{impl{}};
auto *ptr = static_cast<impl *>(poly.data());
ASSERT_TRUE(poly);
ASSERT_NE(poly.data(), nullptr);
ASSERT_NE(std::as_const(poly).data(), nullptr);
ASSERT_EQ(ptr->value, 0);
ASSERT_EQ(poly.get(), 0);
poly.set(1);
poly.incr();
ASSERT_EQ(ptr->value, 2);
ASSERT_EQ(poly.get(), 2);
ASSERT_EQ(poly.mul(3), 6);
poly.decr();
ASSERT_EQ(ptr->value, 1);
ASSERT_EQ(poly.get(), 1);
ASSERT_EQ(poly.mul(3), 3);
}
TEST(Poly, Alias) {
impl instance{};
entt::poly<concept> poly{std::ref(instance)};
ASSERT_TRUE(poly);
ASSERT_NE(poly.data(), nullptr);
ASSERT_NE(std::as_const(poly).data(), nullptr);
ASSERT_EQ(instance.value, 0);
ASSERT_EQ(poly.get(), 0);
poly.set(1);
poly.incr();
ASSERT_EQ(instance.value, 2);
ASSERT_EQ(poly.get(), 2);
ASSERT_EQ(poly.mul(3), 6);
poly.decr();
ASSERT_EQ(instance.value, 1);
ASSERT_EQ(poly.get(), 1);
ASSERT_EQ(poly.mul(3), 3);
}