doc: updated documentation (core, entity, poly)

This commit is contained in:
Michele Caini
2020-12-12 15:13:46 +01:00
parent a974235d10
commit 5f539ddfb7
4 changed files with 251 additions and 186 deletions

3
TODO
View File

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

View File

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

View File

@@ -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.
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([](const auto component) {
// ...
auto storage = registry.storage(info);
```
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.
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.
Once the concept that a poly storage must adhere to has been properly defined,
copying an entity will be as easy as:
```cpp
registry.visit(entity, [&](const auto info) {
auto storage = registry.storage(info);
storage->emplace(registry, other, storage->get(entity));
});
```
Moreover, there exists an overload to _visit_ a specific entity:
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
registry.visit(entity, [](const auto component) {
// ...
registry.visit([&](const auto info) {
auto storage = registry.storage(info);
other.storage(info)->insert(other, storage->data(), storage->raw(), storage->size());
});
```
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.
Where this time `other` represents the destination registry.
### Cloning a registry
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.
A general purpose cloning function could be defined 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);
});
```
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:
```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);
});
```
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.
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

View File

@@ -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:
```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:
Both member functions and free functions are supported to fullfill concepts:
```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>>{};
struct Drawable: entt::type_list<void()> {
// ...
template<typename Type>
using impl = entt::value_list<&print<Type>>;
};
```
Refer to the variable template definition for more details.
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>
>;
};
```
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>
entt::type_list<Type...> as_type_list(const entt::type_list<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()>
> {
// ...
};
```
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.