diff --git a/README.md b/README.md index 7014246c3..03a206d83 100644 --- a/README.md +++ b/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 diff --git a/docs/md/poly.md b/docs/md/poly.md new file mode 100644 index 000000000..d03722656 --- /dev/null +++ b/docs/md/poly.md @@ -0,0 +1,161 @@ +# Crash Course: poly + + +# 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) + + +# 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`](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.
+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: + +```cpp +template +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: + +```cpp +template +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 +inline constexpr auto entt::poly_impl = 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: + +```cpp +template +inline constexpr auto entt::poly_impl> = entt::value_list<&std::vector::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 +void print(Type &self) { self.print(); } + +template +inline constexpr auto entt::poly_impl = entt::value_list<&print>{}; +``` + +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; + +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: + +```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. diff --git a/src/entt/entt.hpp b/src/entt/entt.hpp index b336131b3..583a8e9be 100644 --- a/src/entt/entt.hpp +++ b/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" diff --git a/src/entt/poly/poly.hpp b/src/entt/poly/poly.hpp new file mode 100644 index 000000000..6e4aae0d1 --- /dev/null +++ b/src/entt/poly/poly.hpp @@ -0,0 +1,329 @@ +#ifndef ENTT_POLY_POLY_HPP +#define ENTT_POLY_POLY_HPP + + +#include +#include +#include +#include +#include +#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 class Concept, typename Type> +inline constexpr auto poly_impl = value_list{}; + + +/*! @brief Static virtual table factory. */ +class poly_vtable { + template + [[nodiscard]] static auto * vtable_entry(Ret(*)(Type &, Args...)) { + return +[](any &any, Args... args) -> Ret { + return std::invoke(Candidate, any_cast(any), std::forward(args)...); + }; + } + + template + [[nodiscard]] static auto * vtable_entry(Ret(*)(const Type &, Args...)) { + return +[](const any &any, Args... args) -> Ret { + return std::invoke(Candidate, any_cast &>(any), std::forward(args)...); + }; + } + + template + [[nodiscard]] static auto * vtable_entry(Ret(Type:: *)(Args...)) { + return +[](any &any, Args... args) -> Ret { + return std::invoke(Candidate, any_cast(any), std::forward(args)...); + }; + } + + template + [[nodiscard]] static auto * vtable_entry(Ret(Type:: *)(Args...) const) { + return +[](const any &any, Args... args) -> Ret { + return std::invoke(Candidate, any_cast &>(any), std::forward(args)...); + }; + } + + template + [[nodiscard]] static auto * instance(value_list) { + static const auto vtable = std::make_tuple(vtable_entry(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 class Concept, typename Type> + [[nodiscard]] static auto * instance() { + return instance(poly_impl); + } +}; + + +/*! @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 + 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 + poly_inspector invoke(Args &&... args) const; + + /*! @copydoc invoke */ + template + poly_inspector invoke(Args &&... args); +}; + + +/** + * @brief Poly base class used to inject functionalities into concepts. + * @tparam Poly The outermost poly class. + */ +template +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 + [[nodiscard]] decltype(auto) invoke(const poly_base &self, Args &&... args) const { + const auto &poly = static_cast(self); + return std::get(*poly.vtable)(poly.storage, std::forward(args)...); + } + + /*! @copydoc invoke */ + template + [[nodiscard]] decltype(auto) invoke(poly_base &self, Args &&... args) { + auto &poly = static_cast(self); + return std::get(*poly.vtable)(poly.storage, std::forward(args)...); + } +}; + + +/** + * @brief Shortcut for calling `poly_base::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 +decltype(auto) poly_call(Poly &&self, Args &&... args) { + return std::forward(self).template invoke(self, std::forward(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.
+ * This class aims to make it simple and easy to use. + * + * Below is a minimal example of use: + * + * ```cpp + * template + * struct Drawable: Base { + * void draw() { entt::poly_call<0>(*this); } + * }; + * + * template + * inline constexpr auto entt::poly_impl = entt::value_list<&Type::draw>{}; + * + * using drawable = entt::poly; + * + * 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 class Concept> +class poly: public Concept>> { + /*! @brief A poly base is allowed to snoop into a poly object. */ + friend struct poly_base>; + + using vtable_t = std::remove_pointer_t>())>; + +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 + explicit poly(std::in_place_type_t, Args &&... args) + : storage{std::in_place_type, std::forward(args)...}, + vtable{poly_vtable::instance()} + {} + + /** + * @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 + poly(std::reference_wrapper value) + : storage{value}, + vtable{poly_vtable::instance()} + {} + + /** + * @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>, poly>>> + poly(Type &&value) + : poly{std::in_place_type>>, std::forward(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(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 + void emplace(Args &&... args) { + storage.emplace(std::forward(args)...); + vtable = poly_vtable::instance(); + } + + /** + * @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 diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ac584c9f2..545bda80d 100644 --- a/test/CMakeLists.txt +++ b/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) diff --git a/test/entt/poly/poly.cpp b/test/entt/poly/poly.cpp new file mode 100644 index 000000000..863ce4b7c --- /dev/null +++ b/test/entt/poly/poly.cpp @@ -0,0 +1,148 @@ +#include +#include +#include +#include + +template +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 +void decr(Type &self) { + self.set(self.get()-1); +} + +template +int mul(const Type &self, int v) { + return v * self.get(); +} + +template +inline constexpr auto entt::poly_impl = + entt::value_list< + &Type::incr, + &Type::set, + &Type::get, + &decr, + &mul + >{}; + +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 empty{}; + entt::poly in_place{std::in_place_type, 3}; + entt::poly alias{std::ref(instance)}; + entt::poly 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()); + ASSERT_EQ(alias.type(), entt::type_id()); + ASSERT_EQ(value.type(), entt::type_id()); + + 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()); + ASSERT_EQ(empty.get(), 0); + + empty.emplace(3); + + ASSERT_TRUE(empty); + ASSERT_EQ(empty.get(), 3); + + entt::poly 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()); + ASSERT_EQ(ref.get(), 3); + + entt::poly null{}; + std::swap(empty, null); + + ASSERT_FALSE(empty); + + entt::poly copy = in_place; + + ASSERT_TRUE(copy); + ASSERT_EQ(copy.get(), 3); + + entt::poly move = std::move(copy); + + ASSERT_TRUE(move); + ASSERT_FALSE(copy); + ASSERT_EQ(move.get(), 3); +} + +TEST(Poly, Owned) { + entt::poly poly{impl{}}; + auto *ptr = static_cast(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 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); +}