Просмотр исходного кода

doc: updated documentation (core, entity, poly)

Michele Caini 5 лет назад
Родитель
Сommit
5f539ddfb7
4 измененных файлов с 242 добавлено и 177 удалено
  1. 0 3
      TODO
  2. 2 6
      docs/md/core.md
  3. 34 132
      docs/md/entity.md
  4. 206 36
      docs/md/poly.md

+ 0 - 3
TODO

@@ -27,9 +27,6 @@ WIP:
 * HP: any/poly: configurable sbo size, compile-time policies like sbo-required.
 * HP: registry: use a poly object for pools, no more pool_data type.
 * HP: make runtime views use opaque storage and therefore return also elements.
-* HP: make try_get part of the storage adapter and use it in the poly storage.
-* HP: make poly storage optionally disabled
-* HP: poly support for data members and predefined vtable type (no need to adhere to the api of type)
 * suppress warnings in meta.hpp (uninitialized members)
 * add exclude-only views to combine with packs
 * deprecate non-owning groups in favor of owning views and view packs, introduce lazy owning views

+ 2 - 6
docs/md/core.md

@@ -270,10 +270,10 @@ object:
 
 ```cpp
 // aliasing constructor
-entt::any ref = other.ref();
+entt::any ref = as_ref(other);
 ```
 
-In this case, it doesn't matter if the starting container actually holds an
+In this case, it doesn't matter if the original container actually holds an
 object or acts already as a reference for unmanaged elements, the new instance
 thus created won't create copies and will only serve as a reference for the
 original item.<br/>
@@ -305,11 +305,7 @@ A type info object is an opaque class that is also copy and move constructible.
 This class is returned by the `type_id` function template:
 
 ```cpp
-// generates an info object from a type ...
 auto info = entt::type_id<a_type>();
-
-// ... or directly from a variable
-auto other = entt::type_id(42);
 ```
 
 These are the information made available by this object:

+ 34 - 132
docs/md/entity.md

@@ -26,8 +26,6 @@
     * [Context variables](#context-variables)
     * [Organizer](#organizer)
   * [Meet the runtime](#meet-the-runtime)
-    * [Cloning a registry](#cloning-a-registry)
-    * [Stamping an entity](#stamping-an-entity)
   * [Snapshot: complete vs continuous](#snapshot-complete-vs-continuous)
     * [Snapshot loader](#snapshot-loader)
     * [Continuous loader](#continuous-loader)
@@ -880,153 +878,57 @@ use the preferred tool.
 
 ## Meet the runtime
 
-Type identifiers are stable in `EnTT` during executions and most of the times
-also across different executions and across boundaries. This makes them suitable
-to mix runtime and compile-time features.<br/>
-The registry offers a function to _visit_ it and get the types of components it
-manages:
+`EnTT` takes full advantage of what the language offers at compile-time.<br/>
+However, by combining these feature with a tool for static polymorphism, it's
+also possible to have opaque proxies to work with _type-less_ pools at runtime.
 
-```cpp
-registry.visit([](const auto component) {
-    // ...
-});
-```
-
-Moreover, there exists an overload to _visit_ a specific entity:
+These objects are returned by the `storage` member function, which accepts a
+`type_info` object as an argument rather than a compile-time type (the same
+returned by the `visit` member function):
 
 ```cpp
-registry.visit(entity, [](const auto component) {
-    // ...
-});
+auto storage = registry.storage(info);
 ```
 
-This helps to create a bridge between the registry, that is heavily based on the
-C++ type system, and any other context where the compile-time isn't an option.
-For example: plugin systems, meta system, serialization, and so on.
-
-### Cloning a registry
+By default and to stay true with the philosophy of the library, the API of a
+proxy is minimal and doesn't allow users to do much.<br/>
+However, it's also completely customizable in a generic way and with the
+possibility of defining specific behaviors for given types.
 
-Cloning a registry isn't a suggested practice since it could trigger many copies
-and cut down the performance. Moreover, because of how the `registry` class is
-designed, supporting this as a built-in feature would increase the compilation
-times also for the users that aren't interested in cloning. Even worse, it would
-make difficult to define different _cloning policies_ for different types when
-required.<br/>
-This is why function definitions for cloning have been moved to the user space.
-The `visit` member function of the `registry` class can help filling the gap,
-along with the `insert` functionality.
+This section won't go into detail on how to define a poly storage to get all the
+possible functionalities out of it. `EnTT` already contains enough snippets to
+get inspiration from, both in the test suite and in the `example` folder.<br/>
+In short, users will have to define their own _concepts_ (see the `entt::poly`
+documentation for this) and register them via the `poly_storage_traits` class
+template, which has been designed as sfinae-friendly for the purpose.
 
-A general purpose cloning function could be defined as:
+Once the concept that a poly storage must adhere to has been properly defined,
+copying an entity will be as easy as:
 
 ```cpp
-template<typename Type>
-void clone(const entt::registry &from, entt::registry &to) {
-    const auto *data = from.data<Type>();
-    const auto size = from.size<Type>();
-
-    if constexpr(ENTT_IS_EMPTY(Type)) {
-        to.insert<Type>(data, data + size);
-    } else {
-        const auto *raw = from.raw<Type>();
-        to.insert<Type>(data, data + size, raw, raw + size);
-    }
-}
-```
-
-This is probably the fastest method to inject entities and components in a
-registry that isn't necessarily empty. All new elements are _appended_ to the
-existing ones, if any.<br/>
-This function is also eligible for type erasure in order to create a mapping
-between type identifiers and opaque methods for cloning:
-
-```cpp
-using clone_fn_type = void(const entt::registry &, entt::registry &);
-std::unordered_map<entt::id_type, clone_fn_type *> clone_functions;
-
-// ...
-
-clone_functions[entt::type_hash<position>::value()] = &clone<position>;
-clone_functions[entt::type_hash<velocity>::value()] = &clone<velocity>;
-```
-
-Stamping a registry becomes straightforward with such a mapping then:
-
-```cpp
-entt::registry from;
-entt::registry to;
-
-// ...
-
-from.visit([this, &to](const auto type_id) {
-    clone_functions[type_id](from, to);
+registry.visit(entity, [&](const auto info) {
+    auto storage = registry.storage(info);
+    storage->emplace(registry, other, storage->get(entity));
 });
 ```
 
-Custom cloning functions are also pretty easy to define. Moreover, also cloning
-registries specialized with different identifiers is possible this way.<br/>
-As a side note, cloning functions could be also attached to a reflection system
-where meta types are resolved using the runtime type identifiers.
-
-### Stamping an entity
-
-Using multiple registries at the same time is quite common. Examples are the
-separation of the UI from the simulation or the loading of different scenes in
-the background, possibly on a separate thread, without having to keep track of
-which entity belongs to which scene.<br/>
-In fact, with `EnTT` this is even a recommended practice, as the registry is
-nothing more than an opaque container you can swap at any time.
-
-Once there are multiple registries available, one or more methods are needed to
-transfer information from one container to another though.<br/>
-This is where the `visit` member function of the `registry` class enters the
-game.
-
-Since stamping a component could require different methods for different types
-and not all users want to benefit from this feature, function definitions have
-been moved from the registry to the user space.<br/>
-This helped to reduce compilation times and to allow for maximum flexibility,
-even though it requires users to set up their own stamping functions.
-
-The best bet here is probably to define a reflection system or a mapping between
-the type identifiers and their opaque functions for stamping. As an example:
-
-```cpp
-template<typename Type>
-void stamp(const entt::registry &from, const entt::entity src, entt::registry &to, const entt::entity dst) {
-    to.emplace_or_replace<Type>(dst, from.get<Type>(src));
-}
-```
-
-If the definition above is treated as a general purpose function for stamping,
-one can easily construct a map like the following one as a data member of a
-dedicate system:
+Where `other` is the entity to which the elements should be replicated.<br/>
+Similarly, copying entire pools between different registries can look like this:
 
 ```cpp
-using stamp_fn_type = void(const entt::registry &, const entt::entity, entt::registry &, const entt::entity);
-std::unordered_map<entt::id_type, stamp_fn_type *> stamp_functions;
-
-// ...
-
-stamp_functions[entt::type_hash<position>::value()] = &stamp<position>;
-stamp_functions[entt::type_hash<velocity>::value()] = &stamp<velocity>;
-```
-
-Then _stamp_ entities across different registries as:
-
-```cpp
-entt::registry from;
-entt::registry to;
-
-// ...
-
-from.visit(src, [this, &to, dst](const auto type_id) {
-    stamp_functions[type_id](from, src, to, dst);
+registry.visit([&](const auto info) {
+    auto storage = registry.storage(info);
+    other.storage(info)->insert(other, storage->data(), storage->raw(), storage->size());
 });
 ```
 
-This way it's also pretty easy to define custom stamping functions for _special_
-types if needed. Moreover, stamping entities across registries specialized with
-different identifiers is possibile in practice.
+Where this time `other` represents the destination registry.
+
+So, all in all, `EnTT` shifts the complexity to the one-time definition of a
+_concept_ that reflects the user's needs, and then leaves room for ease of use
+within the codebase.<br/>
+The possibility of extreme customization is the icing on the cake in this sense,
+allowing users to design this tool around their own requirements.
 
 ## Snapshot: complete vs continuous
 

+ 206 - 36
docs/md/poly.md

@@ -8,6 +8,10 @@
 * [Introduction](#introduction)
   * [Other libraries](#other-libraries)
 * [Concept and implementation](#concept-and-implementation)
+  * [Deduced interface](#deduced-interface)
+  * [Defined interface](#defined-interface)
+  * [Fullfill a concept](#fullfill-a-concept)
+* [Inheritance](#inheritance)
 * [Static polymorphism in the wild](#static-polymorphism-in-the-wild)
 <!--
 @endcond TURN_OFF_DOXYGEN
@@ -28,11 +32,10 @@ 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/>
+Since the `poly` class template makes use of `entt::any` internally, it also
+supports most of its feature. Among the most important, the possibility to
+create aliases to existing and thus unmanaged objects. This allows users to
+exploit the static polymorphism while maintaining ownership of 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.
@@ -48,12 +51,12 @@ Among all, the two that I prefer are:
   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/>
+I've some doubts about the usefulness of some feature in real world projects,
+but perhaps my lack of experience 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.
+feature.
 
 Either way, the authors are gurus of the C++ community, people I only have to
 learn from.
@@ -63,18 +66,49 @@ 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.<br/>
-In `EnTT`, this translates into the definition of a template class as follows:
+For this purpose, the library offers a single class that supports both deduced
+and fully defined interfaces. Although having interfaces deduced automatically
+is convenient and allows users to write less code in most cases, this has some
+limitations and it's therefore useful to be able to get around the deduction by
+providing a custom definition for the static virtual table.
+
+Once the interface is defined, it will be sufficient to provide a generic
+implementation to fullfill the concept.<br/>
+Also in this case, the library allows customizations based on types or families
+of types, so as to be able to go beyond the generic case where necessary.
+
+## Deduced interface
+
+This is how a concept with a deduced interface is introduced:
 
 ```cpp
-template<typename Base>
-struct Drawable: Base {
-    void draw() { this->template invoke<0>(*this); }
+struct Drawable: entt::type_list<> {
+    template<typename Base>
+    struct type: 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/>
+It's recognizable by the fact that it inherits from an empty type list.<br/>
+Functions can also be const, accept any number of paramters and return a type
+other than `void`:
+
+```cpp
+struct Drawable: entt::type_list<> {
+    template<typename Base>
+    struct type: Base {
+        bool draw(int pt) const { return this->template invoke<0>(*this, pt); }
+    };
+
+    // ...
+};
+```
+
+In this case, all parameters must be passed to `invoke` after the reference to
+`this` and the return value is whatever the internal call returns.<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
@@ -82,43 +116,172 @@ 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); }
+struct Drawable: entt::type_list<> {
+    template<typename Base>
+    struct type: Base {
+        bool draw() const { 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:
+Once the _concept_ is defined, users must provide a generic implementation of it
+in order to tell the system how any type can satisfy its requirements. This is
+done via an alias template within the concept itself.<br/>
+The index passed as a template parameter to either `invoke` or `poly_call`
+refers to how this alias is defined.
+
+## Defined interface
+
+A fully defined concept is no different to one for which the interface is
+deduced, with the only difference that the list of types is not empty this time:
 
 ```cpp
-template<typename Type>
-inline constexpr auto entt::poly_impl<Drawable, Type> = entt::value_list<&Type::draw>{};
+struct Drawable: entt::type_list<void()> {
+    template<typename Base>
+    struct type: Base {
+        void draw() { entt::poly_call<0>(*this); }
+    };
+
+    // ...
+};
+```
+
+Again, parameters and return values other than `void` are allowed. Also, the
+function type must be const when the method to bind to it is const:
+
+```cpp
+struct Drawable: entt::type_list<bool(int) const> {
+    template<typename Base>
+    struct type: Base {
+        bool draw(int pt) const { return entt::poly_call<0>(*this, pt); }
+    };
+
+    // ...
+};
+```
+
+Why should a user fully define a concept if the function types are the same as
+the deduced ones?<br>
+Because, in fact, this is exactly the limitation that can be worked around by
+manually defining the static virtual table.
+
+When things are deduced, there is an implicit constraint.<br/>
+If the concept exposes a member function called `draw` with function type
+`void()`, a concept can be satisfied:
+
+* Either by a class that exposes a member function with the same name and the
+  same signature.
+
+* Or through a lambda that makes use of existing member functions from the
+  interface itself.
+
+In other words, it's not possible to make use of functions not belonging to the
+interface, even if they are present in the types that fulfill the concept.<br/>
+Similarly, it's not possible to deduce a function in the static virtual table
+with a function type different from that of the associated member function in
+the interface itself.
+
+Explicitly defining a static virtual table suppresses the deduction step and
+allows maximum flexibility when providing the implementation for a concept.
+
+## Fullfill a concept
+
+The `impl` alias template of a concept is used to define how it's fulfilled:
+
+```cpp
+struct Drawable: entt::type_list<> {
+    // ...
+
+    template<typename Type>
+    using 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.<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:
+Both member functions and free functions are supported to fullfill concepts:
 
 ```cpp
 template<typename Type>
-inline constexpr auto entt::poly_impl<Drawable, std::vector<Type>> = entt::value_list<&std::vector<Type>::size>{};
+void print(Type &self) { self.print(); }
+
+struct Drawable: entt::type_list<void()> {
+    // ...
+
+    template<typename Type>
+    using impl = entt::value_list<&print<Type>>;
+};
+```
+
+Likewise, as long as the parameter types and return type support conversions to
+and from those of the function type referenced in the static virtual table, the
+actual implementation may differ in its function type since it's erased
+internally.<br/>
+Moreover, the `self` parameter isn't strictly required by the system and can be
+left out for free functions if not required.
+
+Refer to the inline documentation for more details.
+
+# Inheritance
+
+_Concept inheritance_ is straightforward due to how poly looks like in `EnTT`.
+Therefore, it's quite easy to build hierarchies of concepts if necessary.<br/>
+The only constraint is that all concepts in a hierarchy must belong to the same
+_family_, that is, they must be either all deduced or all defined.
+
+For a deduced concept, inheritance is achieved in a few steps:
+
+```cpp
+struct DrawableAndErasable: entt::type_list<> {
+    template<typename Base>
+    struct type: typename Drawable::type<Base> {
+        static constexpr auto base = std::tuple_size_v<typename entt::poly_vtable<Drawable>::type>;
+        void erase() { entt::poly_call<base + 0>(*this); }
+    };
+
+    template<typename Type>
+    using impl = entt::value_list_cat_t<
+        typename Drawable::impl<Type>,
+        entt::value_list<&Type::erase>
+    >;
+};
 ```
 
-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:
+The static virtual table is empty and must remain so.<br/>
+On the other hand, `type` no longer inherits from `Base` and instead forwards
+its template parameter to the type exposed by the _base class_. Internally, the
+size of the static virtual table of the base class is used as an offset for the
+local indexes.<br/>
+Finally, by means of the `value_list_cat_t` utility, the implementation consists
+in appending the new functions to the previous list.
+
+As for a defined concept instead, also the list of types must be extended, in a
+similar way to what is shown for the implementation of the above concept.<br/>
+To do this, it's useful to declare a function that allows to convert a _concept_
+into its underlying `type_list` object:
 
 ```cpp
-template<typename Type>
-void print(Type &self) { self.print(); }
+template<typename... Type>
+entt::type_list<Type...> as_type_list(const entt::type_list<Type...> &);
+```
 
-template<typename Type>
-inline constexpr auto entt::poly_impl<Drawable, Type> = entt::value_list<&print<Type>>{};
+The definition isn't strictly required, since the function will only be used
+through a `decltype` as it follows:
+
+```cpp
+struct DrawableAndErasable: entt::type_list_cat_t<
+    decltype(as_type_list(std::declval<Drawable>())),
+    entt::type_list<void()>
+> {
+    // ...
+};
 ```
 
-Refer to the variable template definition for more details.
+Similar to above, `type_list_cat_t` is used to concatenate the underlying static
+virtual table with the new function types.<br/>
+Everything else is the same as already shown instead.
 
 # Static polymorphism in the wild
 
@@ -140,10 +303,10 @@ struct square {
 // ...
 
 drawable d{circle{}};
-d.draw();
+d->draw();
 
 d = square{};
-d.draw();
+d->draw();
 ```
 
 The `poly` class template offers a wide range of constructors, from the default
@@ -159,3 +322,10 @@ 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.
+
+Note also how the underlying concept is accessed via a call to `operator->` and
+not directly as `d.draw()`.<br/>
+This allows users to decouple the API of the wrapper from that of the concept.
+Therefore, where `d.data()` will invoke the `data` member function of the poly
+object, `d->data()` will map directly to the functionality exposed by the
+underlying concept.