Michele Caini 5 лет назад
Родитель
Сommit
2f52bef882
6 измененных файлов с 646 добавлено и 2 удалено
  1. 3 2
      README.md
  2. 161 0
      docs/md/poly.md
  3. 1 0
      src/entt/entt.hpp
  4. 329 0
      src/entt/poly/poly.hpp
  5. 4 0
      test/CMakeLists.txt
  6. 148 0
      test/entt/poly/poly.cpp

+ 3 - 2
README.md

@@ -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 - 0
docs/md/poly.md

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

+ 1 - 0
src/entt/entt.hpp

@@ -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 - 0
src/entt/poly/poly.hpp

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

+ 4 - 0
test/CMakeLists.txt

@@ -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 - 0
test/entt/poly/poly.cpp

@@ -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);
+}