Technical Documentation: skypjack/entt
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
[!CAUTION]
EnTThas been a dream so far, we haven't found a single bug to date and it's
super easy to work with.-- Every EnTT User Ever
EnTT is a header-only, tiny and easy to use library for game programming and
much more written in modern C++.
Among others, it's used
in Minecraft by Mojang, the
ArcGIS Runtime SDKs by Esri
and the amazing Ragdoll.
If you don't see your project in the list, please open an issue, submit a PR or
add the #entt tag to your topics! :+1:
Do you want to keep up with changes or do you have a question that
doesn't require you to open an issue?
Join the gitter channel and the
discord server, meet other users like you. The
more we are, the better for everyone.
Don't forget to check the
FAQs and the
wiki too. Your answers may already be
there.
Do you want to support EnTT? Consider becoming a
sponsor or making a
donation via PayPal.
Many thanks to these people and
special thanks to:
[!WARNING]
We apologize to our kind users for not posting updates every three days saying
that we are faster than three days ago and better than everyone else,
but consider it done. In the meantime, enjoy a library that just works
without having to constantly remind ourselves.
Table of Contents
Introduction
The entity-component-system (also known as ECS) is an architectural pattern
used mostly in game development. For further details:
This project started off as a pure entity-component system. Over time the
codebase has grown as more and more classes and functionalities were added.
Here is a brief, yet incomplete list of what it offers today:
- Support for user implementations of the Standard Template Library.
- Built-in RTTI system mostly similar to the standard one.
- Incredibly fast entity-component system with its own pay for what you
use policy, unconstrained component types with optional pointer stability and
hooks for storage customization. - General purpose execution graph builder for optimal scheduling.
- A built-in, non-intrusive and macro-free runtime reflection system.
- Static polymorphism made simple and within everyone's reach.
- A few homemade containers, like a sparse set based hash map.
- All that is needed for resource management (cache, loaders, handles).
- Delegates, signal handlers and a tiny event dispatcher.
- And much more! Check out the
wiki.
This list as well as the project are a work in progress. The whole API is fully
documented for those who are brave enough to read it.
Please, do note that all tools are also DLL-friendly now and run smoothly across
boundaries.
[!NOTE]
A lot of stuff was invented here, but we remain humble and let people copy
it without giving us credit.
The good and the bad of open source, but bragging is for others. Feel
free to get inspired.
One thing known to most is that EnTT is also used in Minecraft.
Given that the game is available literally everywhere, I can confidently say
that the library has been sufficiently tested on every platform that can come to
mind.
Code Example
#include <entt/entt.hpp>
struct position {
float x;
float y;
};
struct velocity {
float dx;
float dy;
};
void update(entt::registry ®istry) {
auto view = registry.view<const position, velocity>();
// use a callback
view.each([](const auto &pos, auto &vel) { /* ... */ });
// use an extended callback
view.each([](const auto entity, const auto &pos, auto &vel) { /* ... */ });
// use a range-for
for(auto [entity, pos, vel]: view.each()) {
// ...
}
// use forward iterators and get only the components of interest
for(auto entity: view) {
auto &vel = view.get<velocity>(entity);
// ...
}
}
int main() {
entt::registry registry;
for(auto i = 0u; i < 10u; ++i) {
const auto entity = registry.create();
registry.emplace<position>(entity, i * 1.f, i * 1.f);
if(i % 2 == 0) { registry.emplace<velocity>(entity, i * .1f, i * .1f); }
}
update(registry);
}Motivation
I started developing EnTT for the wrong reason: my goal was to design an
entity-component system to beat another well known open source library both in
terms of performance and possibly memory usage.
In the end, I did it, but it wasn't very satisfying. Actually it wasn't
satisfying at all. The fastest and nothing more, fairly little indeed. When I
realized it, I tried hard to keep intact the great performance of EnTT and to
add all the features I wanted to see in my own library at the same time.
Nowadays, EnTT is finally what I was looking for: still faster than its
competitors, lower memory usage in the average case, a really good API and an
amazing set of features. And even more, of course.
[!Note]
If theEnTTAPI looks familiar, it's probably because it's been copied a
lot elsewhere (often without much ado).
Credit where credit is due though, the initial design was heavily inspired byEntityX, another popular C++ library.
Benchmark
For what it's worth, you'll never see me trying to make other projects look
bad or offer dubious comparisons just to make this library seem cooler.
I leave this activity to others, if they enjoy it (and it seems that some people
actually like it). I prefer to make better use of my time.
If you are interested, you can compile the benchmark test in release mode (to
enable compiler optimizations, otherwise it would make little sense) by setting
the ENTT_BUILD_BENCHMARK option of CMake to ON, then evaluate yourself
whether you're satisfied with the results or not.
There are also a lot of projects out there that use EnTT as a basis for
comparison (this should already tell you a lot). Many of these benchmarks are
completely wrong, many others are simply incomplete, good at omitting some
information and using the wrong function to compare a given feature. Certainly
there are also good ones but they age quickly if nobody updates them, especially
when the library they are dealing with is actively developed.
Out of all of them, this seems like
the most up-to-date project and also covers a certain number of libraries. I
can't say exactly whether EnTT is used correctly or not. However, even if used
poorly, it should still give the reader an idea of where it's going to operate.
Integration
EnTT is a header-only library. This means that including the entt.hpp header
is enough to include the library as a whole and use it. For those who are
interested only in the entity-component system, consider to include the soleentity/registry.hpp header instead.
It's a matter of adding the following line to the top of a file:
#include <entt/entt.hpp>Use the line below to include only the entity-component system instead:
#include <entt/entity/registry.hpp>Then pass the proper -I argument to the compiler to add the src directory to
the include paths.
Requirements
To be able to use EnTT, users must provide a full-featured compiler that
supports at least C++20.
The requirements below are mandatory to compile the tests and to extract the
documentation:
CMakeversion 3.28 or later.Doxygenversion 1.18 or later.
Alternatively, Bazel is also supported as a build system
(credits to zaucy who offered to maintain it).
In the documentation below I'll still refer to CMake, this being the official
build system of the library.
CMake
To use EnTT from a CMake project, just link an existing target to theEnTT::EnTT alias.
The library offers everything you need for locating (as in find_package),
embedding (as in add_subdirectory), fetching (as in FetchContent) or using
it in many of the ways that you can think of and that involve CMake.
Covering all possible cases would require a treatise and not a simple README
file, but I'm confident that anyone reading this section also knows what it's
about and can use EnTT from a CMake project without problems.
Note that all install calls are guarded by the ENTT_INSTALL option to allow
using EnTT as a submodule without conflicting with user logic.
It is therefore necessary to set the option to true to take advantage of the
installation logic provided by this library.
Natvis support
When using CMake, just enable the option ENTT_INCLUDE_NATVIS and enjoy
it.
Otherwise, most of the tools are covered via Natvis and all files can be found
in the natvis subdirectory, divided by module.
If you spot errors or have suggestions, any contribution is welcome!
Packaging Tools
EnTT is available for some of the most known packaging tools. In particular:
Conan, the C/C++ Package
Manager for Developers.vcpkg, Microsoft VC++ Packaging
Tool.
You can download and installEnTTin just a few simple steps:text$ git clone https://github.com/Microsoft/vcpkg.git $ cd vcpkg $ ./bootstrap-vcpkg.sh $ ./vcpkg integrate install $ vcpkg install enttOr you can use the
experimentalfeature to test the latest changes:textvcpkg install entt[experimental] --headThe
EnTTport invcpkgis kept up to date by Microsoft team members and
community contributors.
If the version is out of date, please
create an issue or pull request on thevcpkgrepository.Homebrew, the missing package
manager for macOS.
Available as a homebrew formula. Just type the following to install it:textbrew install skypjack/entt/enttbuild2, build toolchain for developing and packaging C
and C++ code.
In order to use theenttpackage in abuild2
project, add the following line or a similar one to themanifestfile:textdepends: entt ^3.0.0Also check that the configuration refers to a valid repository, so that the
package can be found bybuild2:cppget.org, the open-source community central
repository, accessible ashttps://pkg.cppget.org/1/stable.Package source repository:
accessible as eitherhttps://github.com/build2-packaging/entt.gitorssh://[email protected]/build2-packaging/entt.git.
Feel free to report issues with
this package.
Both can be used with
bpkg add-repoor added in a projectrepositories.manifest. See the official
documentation
for more details.bzlmod, Bazel's external
dependency management system.
To use theenttmodule in abazelproject, add the following to yourMODULE.bazelfile:starlarkbazel_dep(name = "entt", version = "3.16.0")EnTT will now be available as
@entt(short for@entt//:entt) to be used
in yourcc_*ruledeps.
Consider this list a work in progress and help me to make it longer if you like.
pkg-config
EnTT also supports pkg-config (for some definition of supports at least).
A suitable file called entt.pc is generated and installed in a proper
directory when running CMake.
This should also make it easier to use with tools such as Meson or similar.
Documentation
The documentation is based on doxygen. To build it:
$ cd build
$ cmake .. -DENTT_BUILD_DOCS=ON
$ makeThe API reference is created in HTML format in the build/docs/html directory.
To navigate it with your favorite browser:
$ cd build
$ your_favorite_browser docs/html/index.htmlThe same version is also available online
for the latest release, that is the last stable tag.
Moreover, there exists a wiki dedicated
to the project where users can find all related documentation pages.
Tests
To compile and run the tests, EnTT requires googletest.cmake downloads and compiles the library before compiling anything else. In
order to build the tests, set the CMake option ENTT_BUILD_TESTING to ON.
To build the most basic set of tests:
$ cd build$ cmake -DENTT_BUILD_TESTING=ON ..$ make$ make test
Note that benchmarks are not part of this set.
EnTT in Action
EnTT is widely used in private and commercial applications. I cannot even
mention most of them because of some signatures I put on some documents time
ago. Fortunately, there are also people who took the time to implement open
source projects based on EnTT and did not hold back when it came to
documenting them.
Here you can find an
incomplete list of games, applications and articles that can be used as a
reference.
If you know of other resources out there that are about EnTT, feel free to
open an issue or a PR and I'll be glad to add them to the list.
Extensions
Community-maintained extensions for EnTT:
- EnTTx - Adds hierarchies,
prefabs, and observers for use cases such as networking and undo/redo.
If you know of other EnTT extensions, feel free to open an issue or a PR and
I'll be glad to add them to the list.
Contributors
Requests for features, PRs, suggestions and feedback are highly appreciated.
If you find you can help and want to contribute to the project with your
experience or you do want to get part of the project for some other reason, feel
free to contact me directly (you can find the mail in the
profile).
I can't promise that each and every contribution will be accepted, but I can
assure that I'll do my best to take them all as soon as possible.
If you decide to participate, please see the guidelines for
contributing
before to create issues or pull requests.
Take also a look at the
contributors list to
know who has participated so far.
License
Code and documentation Copyright (c) 2017-2026 Michele Caini.
Colorful logo Copyright (c) 2018-2021 Richard Caseres.
Code released under
the MIT license.
Documentation released under
CC BY 4.0.
All logos released under
CC BY-SA 4.0.
CMakeLists (`docs/CMakeLists.txt`)
Doxygen configuration (documentation)
find_package(Doxygen 1.18)
if(DOXYGEN_FOUND)
include(FetchContent)
FetchContent_Declare(
doxygen-awesome-css
URL https://github.com/jothepro/doxygen-awesome-css/archive/refs/heads/main.zip
)
FetchContent_MakeAvailable(doxygen-awesome-css)
set(DOXY_SOURCE_DIRECTORY ${EnTT_SOURCE_DIR}/src)
set(DOXY_CSS_DIRECTORY ${doxygen-awesome-css_SOURCE_DIR})
set(DOXY_DOCS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
set(DOXY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
configure_file(doxy.in doxy.cfg @ONLY)
add_custom_target(
docs ALL
COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/doxy.cfg
WORKING_DIRECTORY ${EnTT_SOURCE_DIR}
VERBATIM
SOURCES
md/config.md
md/container.md
md/core.md
md/entity.md
md/faq.md
md/graph.md
md/lib.md
md/links.md
md/locator.md
md/meta.md
md/poly.md
md/process.md
md/reference.md
md/resource.md
md/signal.md
md/stl.md
doxy.in
)
if(ENTT_INSTALL)
install(
DIRECTORY ${DOXY_OUTPUT_DIRECTORY}/html
DESTINATION share/${PROJECT_NAME}-${PROJECT_VERSION}/
)
endif()endif()
Md Config (`docs/md/config.md`)
Crash Course: configuration
Table of Contents
Introduction
EnTT has become almost completely customizable over time, in many
respects. These variables are just one of the many ways to customize how it
works.
In the vast majority of cases, users will have no interest in changing the
default parameters. For all other cases, the list of possible configurations
with which it is possible to adjust the behavior of the library at runtime can
be found below.
Definitions
All options are intended as parameters to the compiler (or user-defined macros
within the compilation units, if preferred).
Each parameter can result in internal library definitions. It is not recommended
to try to also modify these definitions, since there is no guarantee that they
will remain stable over time unlike the options below.
ENTT_USE_STL
Intended for testing purposes, it forces the use of built-in replacements of
some parts of the standard library that aren't always available otherwise.EnTT detects these cases on its own, and users should never define this
variable explicitly. However, it's still possible if desired.
ENTT_NO_EXCEPTION
Define this variable without assigning any value to it to turn off exception
handling in EnTT.
This is roughly equivalent to setting the compiler flag -fno-exceptions but is
also limited to this library only.
ENTT_USE_ATOMIC
In general, EnTT does not offer primitives to support multi-threading. Many of
the features can be split over multiple threads without any explicit control and
the user is the one who knows if a synchronization point is required.
However, some internal static data shared between threads should be atomic when
using EnTT from multiple threads, even when dealing with local storage. Define
this macro without assigning any value to it to get the job done.
ENTT_ID_TYPE
entt::id_type is directly controlled by this definition and widely used within
the library.
By default, its type is std::uint32_t. However, users can define a different
default type if necessary.
ENTT_SPARSE_PAGE
It is known that the ECS module of EnTT is based on sparse sets. What is
less known perhaps is that the sparse arrays are paged to reduce memory
usage.
Default size of pages (that is, the number of elements they contain) is 4096 but
users can adjust it if appropriate. In all cases, the chosen value must be a
power of 2.
ENTT_PACKED_PAGE
As it happens with sparse arrays, packed arrays are also paginated. However, in
this case the aim is not to reduce memory usage but to have pointer stability
upon component creation.
Default size of pages (that is, the number of elements they contain) is 1024 but
users can adjust it if appropriate. In all cases, the chosen value must be a
power of 2.
ENTT_ASSERT
For performance reasons, EnTT does not use exceptions or any other control
structures. In fact, it offers many features that result in undefined behavior
if not used correctly.
To get around this, the library relies on a lot of asserts for the purpose of
detecting errors in debug builds. By default, it uses assert internally. Users
are allowed to overwrite its behavior by setting this variable.
ENTT_ASSERT_CONSTEXPR
Usually, an assert within a constexpr function is not a big deal. However, in
case of extreme customizations, it might be useful to differentiate.
For this purpose, EnTT introduces an admittedly badly named variable to make
the job easier in this regard. By default, this variable forwards its arguments
to ENTT_ASSERT.
ENTT_DISABLE_ASSERT
Assertions may in turn affect performance to an extent when enabled. WhetherENTT_ASSERT and ENTT_ASSERT_CONSTEXPR are redefined or not, all asserts can
be disabled at once by means of this definition.
Note that ENTT_DISABLE_ASSERT takes precedence over the redefinition of the
other variables and is therefore meant to disable all controls no matter what.
ENTT_NO_ETO
In order to reduce memory consumption and increase performance, empty types are
never instantiated nor stored by the ECS module of EnTT.
Use this variable to treat these types like all others and therefore to create a
dedicated storage for them.
ENTT_NO_MIXIN
EnTT automatically assigns mixins to all storage types to support signaling
when creating, destroying, and modifying elements.
Mixins can have a (most likely negligible) cost in terms of performance and
compilation time. If unwanted, this macro suppresses automatic generation.
ENTT_STANDARD_CPP
EnTT mixes non-standard language features with others that are perfectly
compliant to offer some of its functionalities.
This definition prevents the library from using non-standard techniques, that
is, functionalities that are not fully compliant with the standard C++.
While there are no known portability issues at the time of this writing, this
should make the library fully portable anyway if needed.
Configuration injection
Configuration variables are provided via code or injected directly from the
outside via a dedicated file.EnTT uses __has_include internally and looks for a specific path, namely<entt/ext/config.h>. This can be provided by the user by setting the include
paths appropriately.
For example, CMake allows users to bind additional include directories to a
target with target_include_directories. See the test suite, and in particular
the config_ext test for a practical example.
Md Container (`docs/md/container.md`)
Crash Course: containers
Table of Contents
Introduction
The standard C++ library offers a wide range of containers and adaptors already.
It is really difficult to do better (although it is very easy to do worse, as
many examples available online demonstrate).EnTT does not try in any way to replace what is offered by the standard. Quite
the opposite, given the widespread use that is made of standard containers.
However, the library also tries to fill a gap in features and functionalities by
making available some containers and adaptors initially developed for internal
use.
This section of the library is likely to grow larger over time. However, for the
moment it is quite small and mainly aimed at satisfying some internal
needs.
For all containers and adaptors made available, full test coverage and stability
over time is guaranteed as usual.
Containers
Dense map
The dense map made available in EnTT is a hash map that aims to return a
packed array of elements, so as to reduce the number of jumps in memory during
iterations.
The implementation is based on sparse sets and each bucket is identified by an
implicit list within the packed array itself.
The interface is very close to its counterpart in the standard library, that is,
the std::unordered_map class.
However, both local and non-local iterators returned by a dense map belong to
the input iterator category although they respectively model the concepts of a
forward iterator type and a random access iterator type.
This is because they return a pair of references rather than a reference to a
pair. In other words, dense maps return a so called proxy iterator the value
type of which is:
std::pair<const Key &, Type &>for non-const iterator types.std::pair<const Key &, const Type &>for const iterator types.
This is quite different from what any standard library map returns and should be
taken into account when looking for a drop-in replacement.
Dense set
The dense set made available in EnTT is a hash set that aims to return a
packed array of elements, so as to reduce the number of jumps in memory during
iterations.
The implementation is based on sparse sets and each bucket is identified by an
implicit list within the packed array itself.
The interface is in all respects similar to its counterpart in the standard
library, that is, the std::unordered_set class.
However, this type of set also supports reverse iteration and therefore offers
all the functions necessary for the purpose (such as rbegin and rend).
Adaptors
Table
The basic_table class is a container adaptor which manages multiple sequential
containers together, treating them as different columns of the same table.
The table alias allows users to provide only the types to handle, usingstd::vector as the default sequential container.
Only a small set of functions is provided, although very close to what the API
of the std::vector class offers.
The internal implementation is purposely supported by a tuple of containers
rather than a container of tuples. The purpose is to allow efficient access to
single columns and not just access to the entire data set of the table.
When a row is accessed, all data is returned in the form of a tuple containing
(possibly const) references to the elements of the row itself.
Similarly, when a table is iterated, tuples of references to table elements are
returned for each row.
Md Core (`docs/md/core.md`)
Crash Course: core functionalities
Table of Contents
- Introduction
- Any as in any type
- Bit
- Compressed pair
- Enum as bitmask
- Hashed strings
- Iterators
- Memory
- Monostate
- Type support
- Unique sequential identifiers
- Utilities
Introduction
EnTT comes with a bunch of core functionalities mostly used by the other parts
of the library.
Many of these tools are also useful in everyday work. Therefore, it is worth
describing them so as not to reinvent the wheel in case of need.
Any as in any type
EnTT offers its own any type. It may seem redundant considering that C++17
introduced std::any, but it is not (hopefully).
First of all, the type returned by an std::any is a const reference to anstd::type_info, an implementation defined class that is not something everyone
wants to see in a software. Furthermore, there is no way to bind it to the type
system of the library and therefore with its integrated RTTI support.
The any API is very similar to that of its most famous counterpart, mainly
because this class serves the same purpose of being an opaque container for any
type of value.
Instances also minimize the number of allocations by relying on a well known
technique called small buffer optimization and a fake vtable.
Creating an object of the any type, whether empty or not, is trivial:
// an empty container
entt::any empty{};
// a container for an int
entt::any any{0};
// in place type construction
entt::any in_place_type{std::in_place_type<int>, 42};
// take ownership of already existing, dynamically allocated objects
entt::any in_place{std::in_place, std::make_unique<int>(42).release()};Alternatively, the make_any function serves the same purpose. It requires to
always be explicit about the type and does not support taking ownership:
entt::any any = entt::make_any<int>(42);In all cases, the any class takes the burden of destroying the contained
element when required, regardless of the storage strategy used for the specific
object.
Furthermore, an instance of any is not tied to an actual type. Therefore, the
wrapper is reconfigured when it is assigned a new object of a type other than
the one it contains.
There is also a way to directly assign a value to the variable contained by anentt::any, without necessarily replacing it. This is especially useful when
the object is used in aliasing mode, as described below:
entt::any any{42};
entt::any value{3};
// assigns by copy
any.assign(value);
// assigns by move
any.assign(std::move(value));The any class performs a check on the type information and whether or not the
original type was copy or move assignable, as appropriate.
In all cases, the assign function returns a boolean value that is true in case
of success and false otherwise.
When in doubt about the type of object contained, the type member function
returns a const reference to the type_info associated with its element, ortype_id<void>() if the container is empty.
The type is also used internally when comparing two any objects:
if(any == empty) { /* ... */ }In this case, before proceeding with a comparison, it is verified that the
type of the two objects is actually the same.
Refer to the EnTT type system documentation for more details about howtype_info works and the possible risks of a comparison.
A particularly interesting feature of this class is that it can also be used as
an opaque container for const and non-const references:
int value = 42;
entt::any any{std::in_place_type<int &>(value)};
entt::any cany = entt::make_any<const int &>(value);
entt::any fwd = entt::forward_as_any(value);
any.emplace<const int &>(value);In other words, whenever any is explicitly told to construct an alias, it
acts as a pointer to the original instance rather than making a copy of it or
moving it internally. The contained object is never destroyed, and users must
ensure that its lifetime exceeds that of the container.
Similarly, it is possible to create non-owning copies of any from an existing
object:
// aliasing constructor
entt::any ref = other.as_ref();In this case, it does not matter if the original container actually holds an
object or is as a reference for unmanaged elements already. The new instance
thus created does not create copies and only serves as a reference for the
original item.
It is worth mentioning that, while everything works transparently when it comes
to non-const references, there are some exceptions when it comes to const
references.
In particular, the data member function invoked on a non-const instance ofany that wraps a const reference returns a null pointer in all cases.
To cast an instance of any to a type, the library offers a set of any_cast
functions in all respects similar to their most famous counterparts.
The only difference is that, in the case of EnTT, they will not raise
exceptions but will only trigger an assert in debug mode, otherwise resulting in
undefined behavior in case of misuse in release mode.
Small buffer optimization
The any class uses a technique called small buffer optimization to reduce
the number of allocations where possible.
The default reserved size for an instance of any is sizeof(double[2]).
However, this is also configurable if needed. In fact, any is defined as an
alias for basic_any<Len>, where Len is the size above.
Users can easily set a custom size or define their own aliases:
using my_any = entt::basic_any<sizeof(double[4])>;This feature, in addition to allowing the choice of a size that best suits the
needs of an application, also offers the possibility of forcing dynamic creation
of objects during construction.
In other terms, if the size is 0, any suppresses the small buffer optimization
and always dynamically allocates objects (except for aliasing cases).
Alignment requirement
The alignment requirement is optional and by default the most stringent (the
largest) for any object whose size is at most equal to the one provided.
It is provided as an optional second parameter following the desired size for
the internal storage:
using my_any = entt::basic_any<sizeof(double[4]), alignof(double[4])>;The basic_any class template inspects the alignment requirements in each case,
even when not provided and may decide not to use the small buffer optimization
in order to meet them.
Bit
Some general purpose utilities, such as the fast module function:
const std::size_t result = entt::fast_mod(value, modulus);Where modulus is necessarily a power of two. This type of operation is far
superior in terms of performance to the basic modulus and for this reason
preferred in many areas.
Compressed pair
Primarily designed for internal use and far from being feature complete, thecompressed_pair class does exactly what it promises: it tries to reduce the
size of a pair by exploiting Empty Base Class Optimization (or EBCO).
This class is not a drop-in replacement for std::pair. However, it offers
enough functionalities to be a good alternative for when reducing memory usage
is more important than having some cool and probably useless feature.
Although the API is very close to that of std::pair (apart from the fact that
the template parameters are inferred from the constructor and therefore there is
no entt::make_compressed_pair), the major difference is that first andsecond are functions for implementation requirements:
entt::compressed_pair pair{0, 3.};
pair.first() = 42;There is not much to describe then. It is recommended to rely on documentation
and intuition. At the end of the day, it is just a pair and nothing more.
Enum as bitmask
Sometimes it is useful to be able to use enums as bitmasks. However, enum
classes are not really suitable for the purpose. Main problem is that they do
not convert implicitly to their underlying type.
The choice is then between using old-fashioned enums (with all their problems
that I do not want to discuss here) or writing ugly code.
Fortunately, there is also a third way: adding enough operators in the global
scope to treat enum classes as bitmasks transparently.
The ultimate goal is to write code like the following (or maybe something more
meaningful, but this should give a grasp and remain simple at the same time):
enum class my_flag {
unknown = 0x01,
enabled = 0x02,
disabled = 0x04
};
const my_flag flags = my_flag::enabled;
const bool is_enabled = !!(flags & my_flag::enabled);The problem with adding all operators to the global scope is that these come
into play even when not required, with the risk of introducing errors that are
difficult to deal with.
However, C++ offers enough tools to get around this problem. In particular, the
library requires users to register the enum classes for which bitmask support
should be enabled:
template<>
struct entt::enum_as_bitmask<my_flag>
: std::true_type
{};This is handy when dealing with enum classes defined by third party libraries
and over which the user has no control. However, it is also verbose and can be
avoided by adding a specific value to the enum class itself:
enum class my_flag {
unknown = 0x01,
enabled = 0x02,
disabled = 0x04,
_entt_enum_as_bitmask
};In this case, there is no need to specialize the enum_as_bitmask traits, sinceEnTT automatically detects the flag and enables the bitmask support.
Once the enum class is registered (in one way or the other), the most common
operators such as &, | but also &= and |= are available for use.
Refer to the official documentation for the full list of operators.
Hashed strings
Hashed strings are human-readable identifiers in the codebase that turn into
numeric values at runtime, thus without affecting performance.
The class has an implicit constexpr constructor that chews a bunch of
characters. Once created, one can get the original string by means of the data
member function or convert the instance into a number.
A hashed string is well suited wherever a constant expression is required. No
string-to-number conversion will take place at runtime if used carefully.
Example of use:
auto load(entt::hashed_string::hash_type resource) {
// uses the numeric representation of the resource to load and return it
}
auto resource = load(entt::hashed_string{"gui/background"});There is also a user defined literal dedicated to hashed strings to make them
more user-friendly:
using namespace entt::literals;
constexpr auto str = "text"_hs;User defined literals in EnTT are enclosed in the entt::literals namespace.
Therefore, the entire namespace or selectively the literal of interest must be
explicitly included before each use, a bit like std::literals.
The class also offers the necessary functionalities to create hashed strings at
runtime:
std::string orig{"text"};
// create a full-featured hashed string...
entt::hashed_string str{orig.c_str()};
// ... or compute only the unique identifier
const auto hash = entt::hashed_string::value(orig.c_str());This possibility should not be exploited in tight loops, since the computation
takes place at runtime and no longer at compile-time. It could therefore affect
performance to some degrees.
Wide characters
The hashed_string class is an alias for basic_hashed_string<char>. To use
the C++ type for wide character representations, there exists also the aliashashed_wstring for basic_hashed_string<wchar_t>.
In this case, the user defined literal to use to create hashed strings on the
fly is _hws:
constexpr auto str = L"text"_hws;The hash type of hashed_wstring is the same as its counterpart.
Conflicts
The hashed string class uses FNV-1a internally to hash strings. Because of the
pigeonhole principle, conflicts are possible. This is a fact.
There is no silver bullet to solve the problem of conflicts when dealing with
hashing functions. In this case, the best solution is likely to give up. That is
all.
After all, human-readable unique identifiers are not something strictly defined
and over which users have not the control. Choosing a slightly different
identifier is probably the best solution to make the conflict disappear in this
case.
Iterators
Writing and working with iterators is not always easy. More often than not it
also leads to duplicated code.EnTT tries to overcome this problem by offering some utilities designed to
make this hard work easier.
Input iterator pointer
When writing an input iterator that returns in-place constructed values if
dereferenced, it is not always straightforward to figure out what value_type
is and how to make it behave like a full-fledged pointer.
Conversely, it would be very useful to have an operator-> available on the
iterator itself that always works without too much complexity.
The input iterator pointer is meant for this. It is a small class that wraps the
in-place constructed value and adds some functions on top of it to make it
suitable for use with input iterators:
struct iterator_type {
using value_type = std::pair<first_type, second_type>;
using pointer = input_iterator_pointer<value_type>;
using reference = value_type;
using difference_type = std::ptrdiff_t;
using iterator_category = std::input_iterator_tag;
// ...
}The library makes extensive use of this class internally. In many cases, thevalue_type of the returned iterators is just an input iterator pointer.
Iota iterator
Waiting for C++20, this iterator accepts an integral value and returns all
elements in a certain range:
entt::iota_iterator first{0};
entt::iota_iterator last{100};
for(; first != last; ++first) {
int value = *first;
// ...
}In the future, views will replace this class. Meanwhile, the library makes some
interesting uses of it when a range of integral values is to be returned to the
user.
Iterable adaptor
Typically, a container class provides begin and end member functions (with
their const counterparts) for iteration.
However, it can happen that a class offers multiple iteration methods or allows
users to iterate different sets of elements.
The iterable adaptor is a utility class that makes it easier to use and access
data in this case.
It accepts a couple of iterators (or an iterator and a sentinel) and offers an
iterable object with all the expected methods like begin, end and whatnot.
The library uses this class extensively.
Think for example of views, which can be iterated to access entities but also
offer a method for obtaining an iterable object that returns tuples of entities
and components at once.
Another example is the registry class which allows users to iterate its storage
by returning an iterable object for the purpose.
Memory
There are a handful of tools within EnTT to interact with memory in one way or
another.
Some are geared towards simplifying the implementation of (internal or external)
allocator aware containers. Others are designed to help the developer with
everyday problems.
The former are very specific and for niche problems. For example, there are
tools designed to help forget the meaning of acronyms like POCCA, POCMA or
POCS.
I will not describe them here in detail. Instead, I recommend reading the inline
documentation to those interested in the subject.
Allocator aware unique pointers
A nasty thing in C++ (at least up to C++20) is the fact that shared pointers
support allocators while unique pointers do not.
There is a proposal at the moment that also shows (among the other things) how
this can be implemented without any compiler support.
The allocate_unique function follows this proposal, making a virtue out of
necessity:
std::unique_ptr<my_type, entt::allocation_deleter<allocator_type>> ptr = entt::allocate_unique<my_type>(allocator, arguments);Although the internal implementation is slightly different from what is proposed
for the standard, this function offers an API that is a drop-in replacement for
the same feature.
Monostate
The monostate pattern is often presented as an alternative to a singleton based
configuration system.
This is exactly its purpose in EnTT. Moreover, this implementation is thread
safe by design (hopefully).
Keys are integral values (easily obtained by hashed strings), values are basic
types like ints or bools. Values of different types can be associated with
each key, even more than one at a time.
Because of this, one should pay attention to use the same type both during an
assignment and when trying to read back the data. Otherwise, there is the risk
to incur in unexpected results.
Example of use:
entt::monostate<entt::hashed_string{"mykey"}>{} = true;
entt::monostate<"mykey"_hs>{} = 42;
// ...
const bool b = entt::monostate<"mykey"_hs>{};
const int i = entt::monostate<entt::hashed_string{"mykey"}>{};Type support
EnTT provides some basic information about types of all kinds.
It also offers additional features that are not yet available in the standard
library or that will never be.
Built-in RTTI support
Runtime type identification support (or RTTI) is one of the most frequently
disabled features in the C++ world, especially in the gaming sector. Regardless
of the reasons for this, it is often a shame not to be able to rely on opaque
type information at runtime.
The library tries to fill this gap by offering a built-in system that does not
serve as a replacement but comes very close to being one and offers similar
information to that provided by its counterpart.
Basically, the whole system relies on a handful of classes. In particular:
The unique sequential identifier associated with a given type:
cppauto index = entt::type_index<a_type>::value();The returned value is not guaranteed to be stable across different runs.
However, it can be very useful as index in associative and unordered
associative containers or for positional accesses in a vector or an array.An external generator can also be used if needed. In fact,
type_indexcan be
specialized by type or constrained with a concept in order to allow more
refined specializations such as:cpptemplate<typename Type> requires requires { { Type::index() } -> std::same_as<entt::id_type>; } struct entt::type_index<Type> { static entt::id_type value() noexcept { return Type::index(); } };Indexes must be sequentially generated in this case.
The tool is widely used withinEnTT. Generating indices not sequentially
would break an assumption and would likely lead to undesired behaviors.The hash value associated with a given type:
cppauto hash = entt::type_hash<a_type>::value();In general, the
valuefunction exposed bytype_hashis alsoconstexpr
but this is not guaranteed for all compilers and platforms (although it is
valid with the most well-known and popular ones).This function can use non-standard features of the language for its own
purposes. This makes it possible to provide compile-time identifiers that
remain stable across different runs.
Users can prevent the library from using these features by means of theENTT_STANDARD_CPPdefinition. In this case, there is no guarantee that
identifiers remain stable across executions. Moreover, they are generated
at runtime and are no longer a compile-time thing.As it happens with
type_index, alsotype_hashcan be specialized or
constrained with a concept in order to customize its behavior globally or on a
per-type or per-traits basis.The name associated with a given type:
cppauto name = entt::type_name<a_type>::value();This value is extracted from some information generally made available by the
compiler in use. Therefore, it may differ depending on the compiler and may be
empty in the event that this information is not available.
For example, given the following class:cppstruct my_type { /* ... */ };The name is
my_typewhen compiled with GCC or CLang andstruct my_type
when MSVC is in use.
Most of the time the name is also retrieved at compile-time and is therefore
always returned through anstd::string_view. Users can easily access it and
modify it as needed, for example by removing the wordstructto normalize
the result.EnTTdoes not do this for obvious reasons, since it would be
creating a new string at runtime otherwise.This function can use non-standard features of the language for its own
purposes. Users can prevent the library from using these features by means of
theENTT_STANDARD_CPPdefinition. In this case, the name is just empty.As it happens with
type_index, alsotype_namecan be specialized or
constrained with a concept in order to customize its behavior globally or on a
per-type or per-traits basis.
These are then combined into utilities that aim to offer an API that is somewhat
similar to that made available by the standard library.
Type info
The type_info class is not a drop-in replacement for std::type_info but can
provide similar information which are not implementation defined and do not
require to enable RTTI.
Therefore, they can sometimes be even more reliable than those obtained
otherwise.
Its type defines an opaque class that is also copyable and movable.
Objects of this type are generally returned by the type_id functions:
// by type
auto info = entt::type_id<a_type>();
// by value
auto other = entt::type_id(42);All elements thus received are nothing more than const references to instances
of type_info with static storage duration.
This is convenient for saving the entire object aside for the cost of a pointer.
However, nothing prevents from constructing type_info objects directly:
entt::type_info info{std::in_place_type<int>};These are the information made available by type_info:
The index associated with a given type:
cppauto idx = entt::type_id<a_type>().index();This is also an alias for the following:
cppauto idx = entt::type_index<std::remove_cvref_t<a_type>>::value();The hash value associated with a given type:
cppauto hash = entt::type_id<a_type>().hash();This is also an alias for the following:
cppauto hash = entt::type_hash<std::remove_cvref_t<a_type>>::value();The name associated with a given type:
cppauto name = entt::type_id<my_type>().name();This is also an alias for the following:
cppauto name = entt::type_name<std::remove_cvref_t<a_type>>::value();
Where all accessed features are available at compile-time, the type_info class
is also fully constexpr. However, this cannot be guaranteed in advance and
depends mainly on the compiler in use and any specializations of the classes
described above.
Almost unique identifiers
Since the default non-standard, compile-time implementation of type_hash makes
use of hashed strings, it may happen that two types are assigned the same hash
value.
In fact, although this is quite rare, it is not entirely excluded.
Another case where two types are assigned the same identifier is when classes
from different contexts (for example two or more libraries loaded at runtime)
have the same fully qualified name. In this case, type_name returns the same
value for the two types.
Fortunately, there are several easy ways to deal with this:
The most trivial one is to define the
ENTT_STANDARD_CPPmacro. Runtime
identifiers do not suffer from the same problem in fact. However, this
solution does not work well with a plugin system, where the libraries are not
linked.Another possibility is to specialize the
type_nameclass for one of the
conflicting types, in order to assign it a custom identifier. This is probably
the easiest solution that also preserves the feature of the tool.A fully customized identifier generation policy (based for example on enum
classes or preprocessing steps) may represent yet another option.
These are just some examples of possible approaches to the problem, but there
are many others. As already mentioned above, since users have full control over
their types, this problem is in any case easy to solve and should not worry too
much.
In all likelihood, it will never happen to run into a conflict anyway.
Type traits
A handful of utilities and traits not present in the standard template library
but which can be useful in everyday life.
This list is not exhaustive and contains only some of the most useful
classes. Refer to the inline documentation for more information on the features
offered by this module.
Size of
The standard operator sizeof complains if users provide it with functions or
incomplete types. On the other hand, it is guaranteed that its result is always
non-zero, even if applied to an empty class type.
This small class combines the two and offers an alternative to sizeof that
works under all circumstances, returning zero if the type is not supported:
const auto size = entt::size_of_v<void>;Is applicable
The standard library offers the great std::is_invocable trait in several
forms. This takes a function type and a series of arguments and returns true if
the condition is satisfied.
Moreover, users are also provided with std::apply, a tool for combining
invocable elements and tuples of arguments.
It would therefore be a good idea to have a variant of std::is_invocable that
also accepts its arguments in the form of a tuple-like type, so as to complete
the offer:
constexpr bool result = entt::is_applicable<Func, std::tuple<a_type, another_type>>;This trait is built on top of std::is_invocable and does nothing but unpack a
tuple-like type and simplify the code at the call site.
Constness as
A utility to easily transfer the constness of a type to another type:
// type is const dst_type because of the constness of src_type
using type = entt::constness_as_t<dst_type, const src_type>;The trait is subject to the rules of the language. For example, transferring
constness between references will not give the desired effect.
Member class type
The auto template parameter introduced with C++17 made it possible to simplify
many class templates and template functions but also made the class type opaque
when members are passed as template arguments.
The purpose of this utility is to extract the class type in a few lines of code:
template<typename Member>
using clazz = entt::member_class_t<Member>;N-th argument
A utility to quickly find the n-th argument of a function, member function or
data member (for blind operations on opaque types):
using type = entt::nth_argument_t<1u, decltype(&clazz::member)>;Disambiguation of overloaded functions is the responsibility of the user, should
it be needed.
Integral constant
Since std::integral_constant may be annoying because of its form that requires
to specify both a type and a value of that type, there is a more user-friendly
shortcut for the creation of integral constants.
This shortcut is the alias template entt::integral_constant:
constexpr auto constant = entt::integral_constant<42>;Among the other uses, when combined with a hashed string it helps to define tags
as human-readable names where actual types would be required otherwise:
constexpr auto enemy_tag = entt::integral_constant<"enemy"_hs>;
registry.emplace<enemy_tag>(entity);Tag
Type id_type is very important and widely used in EnTT. Therefore, there is
a more user-friendly shortcut for the creation of constants based on it.
This shortcut is the alias template entt::tag.
If used in combination with hashed strings, it helps to use human-readable names
where types would be required otherwise. As an example:
registry.emplace<entt::tag<"enemy"_hs>>(entity);However, this is not the only permitted use. Literally any value convertible toid_type is a good candidate, such as the named constants of an unscoped enum.
Type list and value list
There is no respectable library where the much desired type list can be
missing.EnTT is no exception and provides (making extensive use of it internally) thetype_list type, in addition to its value_list counterpart dedicated to
non-type template parameters.
Here is a (possibly incomplete) list of the functionalities that come with a
type list:
type_list_element[_t]to get the N-th element of a type list.type_list_index[_v]to get the index of a given element of a type list.type_list_cat[_t]and a handyoperator+to concatenate type lists.type_list_unique[_t]to remove duplicate types from a type list.type_list_contains[_v]to know if a type list contains a given type.type_list_diff[_t]to remove types from type lists.type_list_transform[_t]to transform a range and create another type list.
I am also pretty sure that more and more utilities will be added over time as
needs become apparent.
Many of these functionalities also exist in their version dedicated to value
lists. We therefore have value_list_element[_v] as well asvalue_list_cat[_t]and so on.
Unique sequential identifiers
Sometimes it is useful to be able to give unique, sequential numeric identifiers
to types either at compile-time or runtime.
There are plenty of different solutions for this out there, and I could have
used one of them. However, I decided to spend my time to define a couple of
tools that fully embrace what modern C++ has to offer.
Compile-time generator
To generate sequential numeric identifiers at compile-time, EnTT offers theident class template:
// defines the identifiers for the given types
using id = entt::ident<a_type, another_type>;
// ...
switch(a_type_identifier) {
case id::value<a_type>:
// ...
break;
case id::value<another_type>:
// ...
break;
default:
// ...
}This is what this class template has to offer: a value inline variable that
contains a numeric identifier for the given type. It can be used in any context
where constant expressions are required.
As long as the list remains unchanged, identifiers are also guaranteed to be
stable across different runs. If used in a production environment where a type
needs to be removed, a placeholder can help to leave the other identifiers the
same:
template<typename>
struct ignore_type {};
using id = entt::ident<
a_type_still_valid,
ignore_type<no_longer_valid_type>,
another_type_still_valid
>;Perhaps a bit ugly to see in a codebase, but it gets the job done at least.
Runtime generator
The family class template helps to generate sequential numeric identifiers for
types at runtime:
// defines a custom generator
using id = entt::family<struct my_tag>;
// ...
const auto a_type_id = id::value<a_type>;
const auto another_type_id = id::value<another_type>;This is what a family has to offer: a value inline variable that contains a
numeric identifier for the given type.
The generator is customizable, so as to get different sequences for different
purposes if needed.
Identifiers are not guaranteed to be stable across different runs. Indeed it
mostly depends on the flow of execution.
Utilities
It is not possible to escape the temptation to add utilities of some kind to a
library. In fact, EnTT also provides a handful of tools to simplify the
life of developers:
entt::overload: a tool to disambiguate different overloads from their
function type. It works with both free and member functions.
Consider the following definition:cppstruct clazz { void bar(int) {} void bar() {} };This utility can be used to get the right overload as:
cppauto *member = entt::overload<void(int)>(&clazz::bar);The line above is literally equivalent to:
cppauto *member = static_cast<void(clazz:: *)(int)>(&clazz::bar);Just easier to read and shorter to type.
entt::overloaded: a small class template used to create a new type with an
overloadedoperator()from a bunch of lambdas or functors.
As an example:cppentt::overloaded func{ [](int value) { /* ... */ }, [](char value) { /* ... */ } }; func(42); func('c');Rather useful when doing metaprogramming and having to pass to a function a
callable object that supports multiple types at once.entt::y_combinator: this is a C++ implementation of the y-combinator.
If it is not clear what it is, there is probably no need for this
utility.
Below is a small example to show its use:cppentt::y_combinator gauss([](const auto &self, auto value) -> unsigned int { return value ? (value + self(value-1u)) : 0; }); const auto result = gauss(3u);Maybe convoluted at first glance but certainly effective. Unfortunately,
the language does not make it possible to do much better.
This is a rundown of the (actually few) utilities made available by EnTT. The
list will probably grow over time, but the size of each will remain rather
small, as has been the case so far.
Md Entity (`docs/md/entity.md`)
Crash Course: entity-component system
Table of Contents
- Introduction
- Design decisions
- Vademecum
- The Registry, the Entity and the Component
- Storage
- Meet the runtime
- Views and Groups
- Multithreading
- Beyond this document
Introduction
EnTT offers a header-only, tiny and easy to use entity-component system module
written in modern C++.
The entity-component-system (also known as ECS) is an architectural pattern
used mostly in game development.
Design decisions
Type-less and bitset-free
The library implements a sparse set based model that does not require users to
specify the set of components neither at compile-time nor at runtime.
This is why users can instantiate the core class simply like:
entt::registry registry;In place of its more annoying and error-prone counterpart:
entt::registry<comp_0, comp_1, ..., comp_n> registry;Furthermore, it is not necessary to announce the existence of a component type.
When the time comes, just use it and that is all.
Build your own
The ECS module (as well as the rest of the library) is designed as a set of
containers that are used as needed, just like a vector or any other container.
It does not attempt in any way to take over on the user codebase, nor to control
its main loop or process scheduling.
Unlike other more or less well known models, it also makes use of independent
pools that are extended via static mixins. The built-in signal support is an
example of this flexible design: defined as a mixin, it is easily disabled if
not needed. Similarly, the storage class has a specialization that shows how
everything is customizable down to the smallest detail.
Pay per use
Everything is designed around the principle that users only have to pay for what
they want.
When it comes to using an entity-component system, the tradeoff is usually
between performance and memory usage. The faster it is, the more memory it uses.
Even worse, some approaches tend to heavily affect other functionalities like
the construction and destruction of components to favor iterations, even when it
is not strictly required. In fact, slightly worse performance along non-critical
paths are the right price to pay to reduce memory usage and have overall better
performance.EnTT follows a completely different approach. It gets the best out from the
basic data structures and gives users the possibility to pay more for higher
performance where needed.
All or nothing
As a rule of thumb, a T ** pointer (or whatever a custom pool returns) is
always available to directly access all the instances of a given component typeT.
This is one of the corner stones of the library. Many of the tools offered are
designed around this need and give the possibility to get this information.
Vademecum
The entt::entity type implements the concept of entity identifier. An entity
(the E of an ECS) is an opaque element to use as-is. Inspecting it is not
recommended since its format can change in future.
Components (the C of an ECS) are of any type, without any constraints, not
even that of being movable. No need to register them nor their types.
Systems (the S of an ECS) are plain functions, functors, lambdas and so on.
It is not required to announce them in any case and have no requirements.
The next sections go into detail on how to use the entity-component system part
of the EnTT library.
This module is likely larger than what is described below. For more details,
please refer to the inline documentation.
The Registry, the Entity and the Component
A registry stores and manages entities (or identifiers) and components.
The class template basic_registry lets users decide what the preferred type to
represent an entity is. Because std::uint32_t is large enough for almost any
case, there also exists the enum class entt::entity that wraps it and the
alias entt::registry for entt::basic_registry<entt::entity>.
Entities are represented by entity identifiers. An entity identifier contains
information about the entity itself and its version.
User defined identifiers are allowed as enum classes and class types that define
an entity_type member of type std::uint32_t or std::uint64_t.
A registry is used both to construct and to destroy entities:
// constructs a naked entity with no components and returns its identifier
auto entity = registry.create();
// destroys an entity and all its components
registry.destroy(entity);The create member function also accepts a hint. Moreover, it has an overload
that gets two iterators to use to generate many entities at once efficiently.
Similarly, the destroy member function also works with a range of entities:
// destroys all the entities in a range
auto view = registry.view<a_component, another_component>();
registry.destroy(view.begin(), view.end());In addition to offering an overload to force the version upon destruction.
This function removes all components from an entity before releasing it. There
also exists a lighter alternative that does not query component pools, for use
with orphaned entities:
// releases an orphaned identifier
registry.release(entity);As with the destroy function, also in this case entity ranges are supported
and it is possible to force a version.
In both cases, when an identifier is released, the registry can freely reuse it
internally. In particular, the version of an entity is increased (unless the
overload that forces a version is used instead of the default one).
Users can then test identifiers by means of a registry:
// returns true if the entity is still valid, false otherwise
bool b = registry.valid(entity);
// gets the actual version for the given entity
auto curr = registry.current(entity);Or inspect them using some functions meant for parsing an identifier as-is,
such as:
// gets the version contained in the entity identifier
auto version = entt::to_version(entity);Components are assigned to or removed from entities at any time.
The emplace member function template creates, initializes and assigns to an
entity the given component. It accepts a variable number of arguments to use to
construct the component itself:
registry.emplace<position>(entity, 0., 0.);
// ...
auto &vel = registry.emplace<velocity>(entity);
vel.dx = 0.;
vel.dy = 0.;The default storage detects aggregate types internally and exploits aggregate
initialization when possible.
Therefore, it is not strictly necessary to define a constructor for each type.
The insert member function works with ranges and is used to:
Assign the same component to all entities at once when a type is specified as
a template parameter or an instance is passed as an argument:cpp// default initialized type assigned by copy to all entities registry.insert<position>(first, last); // user-defined instance assigned by copy to all entities registry.insert(from, to, position{0., 0.});Assign a set of components to the entities when a range is provided (the
length of the range of components must be the same of that of entities):cpp// first and last specify the range of entities, instances points to the first element of the range of components registry.insert<position>(first, last, instances);
If an entity already has the given component, the replace and patch member
function templates are used to update it:
// replaces the component in-place
registry.patch<position>(entity, [](auto &pos) { pos.x = pos.y = 0.; });
// constructs a new instance from a list of arguments and replaces the component
registry.replace<position>(entity, 0., 0.);When it is unknown whether an entity already owns an instance of a component,emplace_or_replace is the function to use instead:
registry.emplace_or_replace<position>(entity, 0., 0.);This is a slightly faster alternative to the following snippet:
if(registry.all_of<velocity>(entity)) {
registry.replace<velocity>(entity, 0., 0.);
} else {
registry.emplace<velocity>(entity, 0., 0.);
}The all_of and any_of member functions may also be useful if in doubt about
whether or not an entity has all the components in a set or any of them:
// true if entity has all the given components
bool all = registry.all_of<position, velocity>(entity);
// true if entity has at least one of the given components
bool any = registry.any_of<position, velocity>(entity);If the goal is to delete a component from an entity that owns it, the erase
member function template is the way to go:
registry.erase<position>(entity);When in doubt whether the entity owns the component, use the remove member
function instead. It behaves similarly to erase but it drops the component if
and only if it exists, otherwise it returns safely to the caller:
registry.remove<position>(entity);The clear member function works similarly and is used to either:
Erases all instances of the given components from the entities that own them:
cppregistry.clear<position>();Or destroy all entities in a registry at once:
cppregistry.clear();
Finally, references to components are obtained simply as:
const auto &cregistry = registry;
// const and non-const reference
const auto &crenderable = cregistry.get<renderable>(entity);
auto &renderable = registry.get<renderable>(entity);
// const and non-const references
const auto [cpos, cvel] = cregistry.get<position, velocity>(entity);
auto [pos, vel] = registry.get<position, velocity>(entity);If the existence of the component is not certain, try_get is the more suitable
function instead.
Observe changes
By default, each storage comes with a mixin that adds signal support to it.
This allows for fancy things like dependencies and reactive systems.
The on_construct member function returns a sink (which is an object for
connecting and disconnecting listeners) for those interested in notifications
when a new instance of a given component type is created:
// connects a free function
registry.on_construct<position>().connect<&my_free_function>();
// connects a member function
registry.on_construct<position>().connect<&my_class::member>(instance);
// disconnects a free function
registry.on_construct<position>().disconnect<&my_free_function>();
// disconnects a member function
registry.on_construct<position>().disconnect<&my_class::member>(instance);Similarly, on_destroy and on_update are used to receive notifications about
the destruction and update of an instance, respectively.
Because of how C++ works, listeners attached to on_update are only invoked
following a call to replace, emplace_or_replace or patch.
Runtime pools are also supported by providing an identifier to the functions
above:
registry.on_construct<position>("other"_hs).connect<&my_free_function>();Refer to the following sections for more information about runtime pools.
In all cases, the function type of a listener is equivalent to the following:
void(entt::registry &, entt::entity);All listeners are provided with the registry that triggered the notification and
the involved entity. Note also that:
Listeners for construction signals are invoked after components have been
created.Listeners designed to observe changes are invoked after components have
been updated.Listeners for destruction signals are invoked before components have been
destroyed.
There are also some limitations on what a listener can and cannot do:
Connecting and disconnecting other functions from within the body of a
listener should be avoided. It can lead to undefined behavior in some cases.Removing the component from within the body of a listener that observes the
construction or update of instances of a given type is not allowed.Assigning and removing components from within the body of a listener that
observes the destruction of instances of a given type should be avoided. It
can lead to undefined behavior in some cases. This type of listeners is
intended to provide users with an easy way to perform cleanup and nothing
more.
Please, refer to the documentation of the signal class to know about all the
features it offers.
There are many useful but less known functionalities that are not described
here, such as the connection objects or the possibility to attach listeners with
a list of parameters that is shorter than that of the signal itself.
Auto-binding
Users don't need to create bindings manually each and every time. For managed
types, they can have EnTT setup listeners automatically.
The library searches the types for functions with specific names and signatures,
as in the following example:
struct my_type {
static void on_construct(entt::registry ®istry, const entt::entity entt);
static void on_update(entt::registry ®istry, const entt::entity entt);
static void on_destroy(entt::registry ®istry, const entt::entity entt);
// ...
};As soon as a storage is created for such a defined type, these functions are
associated with the respective signals. The function name is self-explanatory of
the target signal.
Entity lifecycle
Observing entities is also possible. In this case, the user must use the entity
type instead of the component type:
registry.on_construct<entt::entity>().connect<&my_listener>();Since entity storage is unique within a registry, if a name is provided it is
ignored and therefore discarded.
As for the function signature, this is exactly the same as the components.
Entities support all types of signals: construct, destroy, and update. The
latter is perhaps ambiguous as an entity is not truly updated. Rather, its
identifier is created and finally released.
Indeed, the update signal is meant to send general notifications regarding an
entity. It can be triggered via the patch function, as is the case with
components:
registry.patch<entt::entity>(entity);Destroying an entity and then updating the version of an identifier does not
give rise to these types of signals under any circumstances instead.
Finally, note that listeners that observe the destruction of an entity are
invoked after all components have been removed, not before. This is
because the entity would be invalidated before deleting its elements otherwise,
making it difficult for the user to write component listeners.
Listeners disconnection
The destruction order of the storage classes and therefore the disconnection of
the listeners is completely random.
There are no guarantees today, and while the logic is easily discerned, it is
not guaranteed that it will remain so in the future.
For example, a listener getting disconnected after a component is discarded as a
result of pool destruction is most likely a recipe for problems.
Rather, it is advisable to invoke the clear function of the registry before
destroying it. This forces the deletion of all components and entities without
ever discarding the pools.
As a result, a listener that wants to access components, entities, or pools can
safely do so against a still valid registry, while checking for the existence of
the various elements as appropriate.
They call me reactive storage
Signals are the basic tools to construct reactive systems, even if they are not
enough on their own. EnTT tries to take another step in that direction with
its reactive mixin.
In order to explain what reactive systems are, this is a slightly revised quote
from the documentation of the library that first introduced this tool,
Entitas:
Imagine you have 100 fighting units on the battlefield, but only 10 of them
changed their positions. Instead of using a normal system and updating all 100
entities depending on the position, you can use a reactive system which will
only update the 10 changed units. So efficient.
In EnTT, this means iterating over a reduced set of entities and components
than what would otherwise be returned from a view or group.
On these words, however, the similarities with the proposal of Entitas also
end. The rules of the language and the design of the library obviously impose
and allow different things.
A reactive mixin can be used on a standalone storage with any value type
(perhaps using an alias to simplify its use):
using reactive_storage = entt::reactive_mixin<entt::storage<void>>;
entt::registry registry{};
reactive_storage storage{};
storage.bind(registry);In this case, it must be provided with a reference registry for subsequent
operations.
Alternatively, when using the value type provided by EnTT, it is also possible
to create a reactive storage directly inside a registry:
entt::registry registry{};
auto &storage = registry.storage<entt::reactive>("observer"_hs);In the latter case, there is the advantage that, in the event of destruction of
an entity, this storage is also automatically cleaned up.
Also note that, unlike all other storage, these classes do not support signals
by default (although they can be enabled if necessary).
Once it has been created and associated with a registry, the reactive mixin
needs to be informed about what it should observe.
Here the choice boils down to three main events affecting all elements (entities
or components), namely creation, update or destruction:
storage
// observe position component construction
.on_construct<position>()
// observe velocity component update
.on_update<velocity>()
// observe renderable component destruction
.on_destroy<renderable>();It goes without saying that it is possible to observe multiple events of the
same type or of different types with the same storage.
For example, to know which entities have been assigned or updated a component of
a certain type:
storage
.on_construct<my_type>()
.on_update<my_type>();Note that all configurations are in or and never in and. Therefore, to track
entities that have been assigned two different components, there are a couple of
options:
Create two reactive storage, then combine them in a view:
cppfirst_storage.on_construct<position>(); second_storage.on_construct<velocity>(); for(auto entity: entt::basic_view{first_storage, second_storage}) { // ... }Use a reactive storage with a non-
voidvalue type and a custom tracking
function for the purpose:cppusing my_reactive_storage = entt::reactive_mixin<entt::storage<bool>>; void callback(my_reactive_storage &storage, const entt::registry &, const entt::entity entity) { storage.contains(entity) ? (storage.get(entity) = true) : storage.emplace(entity, false); } // ... my_reactive_storage storage{}; storage .on_construct<position, &callback>() .on_construct<velocity, &callback>(); // ... for(auto [entity, both_were_added]: storage.each()) { if(both_were_added) { // ... } }
As highlighted in the last example, the reactive mixin tracks the entities that
match the given conditions and saves them aside. However, this behavior can be
changed.
For example, it is possible to capture all and only the entities for which a
certain component has been updated but only if a specific value is within a
given range:
void callback(reactive_storage &storage, const entt::registry ®istry, const entt::entity entity) {
storage.remove(entity);
if(const auto x = registry.get<position>(entity).x; x >= min_x && x <= max_x) {
storage.emplace(entity);
}
}
// ...
storage.on_update<position, &callback>();This makes reactive storage extremely flexible and usable in a large number of
cases.
Finally, once the entities of interest have been collected, it is possible to
visit the storage like any other:
for(auto entity: storage) {
// ...
}Wrapping it in a view and combining it with other views is another option:
for(auto [entity, pos]: (entt:.basic_view{storage} | registry.view<position>(entt::exclude<velocity>)).each()) {
// ...
}In order to simplify this last use case, the reactive mixin also provides a
specific function that returns a view of the storage already filtered according
to the provided requirements:
for(auto [entity, pos]: storage.view<position>(entt::exclude<velocity>).each()) {
// ...
}The registry used in this case is the one associated with the storage and also
available via the registry function.
It should be noted that a reactive storage never deletes its entities (and
elements, if any). To process and then discard entities at regular intervals,
refer to the clear function available by default for each storage type.
Similarly, the reactive mixin does not disconnect itself from observed storages
upon destruction. Therefore, users have to do this themselves:
entt::registry = storage.registry();
registry.on_construct<position>().disconnect(&storage);
registry.on_construct<velocity>().disconnect(&storage);Destroying a reactive storage without disconnecting it from observed pools will
result in undefined behavior.
Sorting: is it possible?
Sorting entities and components is possible using an in-place algorithm that
does not require memory allocations and is therefore quite convenient.
There are two functions that respond to slightly different needs:
Components are sorted either directly:
cppregistry.sort<renderable>([](const renderable &lhs, const renderable &rhs) { return lhs.z < rhs.z; });Or by accessing their entities:
cppregistry.sort<renderable>([](const entt::entity lhs, const entt::entity rhs) { return entt::registry::entity(lhs) < entt::registry::entity(rhs); });There exists also the possibility to use a custom sort function object for
when the usage pattern is known.Components are sorted according to the order imposed by another component:
cppregistry.sort<movement, physics>();In this case, instances of
movementare arranged in memory so that cache
misses are minimized when the two components are iterated together.
As a side note, the use of groups limits the possibility of sorting pools of
components. Refer to the specific documentation for more details.
Helpers
The so-called helpers are small classes and functions mainly designed to offer
built-in support for the most basic functionalities.
Null entity
The entt::null variable models the concept of a null entity.
The library guarantees that the following expression always returns false:
registry.valid(entt::null);A registry rejects the null entity in all cases because it is not considered
valid. It also means that the null entity cannot own components.
The type of the null entity is internal and should not be used for any purpose
other than defining the null entity itself. However, there exist implicit
conversions from the null entity to identifiers of any allowed type:
entt::entity null = entt::null;Similarly, the null entity compares to any other identifier:
const auto entity = registry.create();
const bool null = (entity == entt::null);As for its integral form, the null entity only affects the entity part of an
identifier and is instead completely transparent to its version.
Be aware that entt::null and entity 0 are different. Likewise, a zero
initialized entity is not the same as entt::null. Therefore, althoughentt::entity{} is in some sense an alias for entity 0, none of them are used
to create a null entity.
Tombstone
Similar to the null entity, the entt::tombstone variable models the concept of
a tombstone.
Once created, the integral form of the two values is the same, although they
affect different parts of an identifier. In fact, the tombstone only uses the
version part of it and is completely transparent to the entity part.
Also in this case, the following expression always returns false:
registry.valid(entt::tombstone);Moreover, users cannot set the tombstone version when releasing an entity:
registry.destroy(entity, entt::tombstone);In this case, a different version number is implicitly generated.
The type of a tombstone is internal and can change at any time. However, there
exist implicit conversions from a tombstone to identifiers of any allowed type:
entt::entity null = entt::tombstone;Similarly, the tombstone compares to any other identifier:
const auto entity = registry.create();
const bool tombstone = (entity == entt::tombstone);Be aware that entt::tombstone and entity 0 are different. Likewise, a zero
initialized entity is not the same as entt::tombstone. Therefore, althoughentt::entity{} is in some sense an alias for entity 0, none of them are used
to create tombstones.
To entity
This function accepts a storage and an instance of a component of the storage
type, then it returns the entity associated with the latter:
const auto entity = entt::to_entity(registry.storage<position>(), instance);Where instance is a component of type position. A null entity is returned in
case the instance does not belong to the registry.
Dependencies
The registry class is designed to create short circuits between its member
functions. This greatly simplifies the definition of a dependency.
For example, the following adds (or replaces) the component a_type whenevermy_type is assigned to an entity:
registry.on_construct<my_type>().connect<&entt::registry::emplace_or_replace<a_type>>();Similarly, the code below removes a_type from an entity whenever my_type is
assigned to it:
registry.on_construct<my_type>().connect<&entt::registry::remove<a_type>>();A dependency is easily broken as follows:
registry.on_construct<my_type>().disconnect<&entt::registry::emplace_or_replace<a_type>>();There are many other types of dependencies. In general, most of the functions
that accept an entity as their first argument are good candidates for this
purpose.
Invoke
The invoke helper allows to propagate a signal to a member function of a
component without having to extend it:
registry.on_construct<clazz>().connect<entt::invoke<&clazz::func>>();All it does is pick up the right component for the received entity and invoke
the requested method, passing on the arguments if necessary.
Connection helper
Connecting signals can quickly become cumbersome.
This utility aims to simplify the process by grouping the calls:
entt::sigh_helper{registry}
.with<position>()
.on_construct<&a_listener>()
.on_destroy<&another_listener>()
.with<velocity>("other"_hs)
.on_update<yet_another_listener>();Runtime pools are also supported by providing an identifier when calling with,
as shown in the previous snippet. Refer to the following sections for more
information about runtime pools.
Obviously, this helper does not make the code disappear but it should at least
reduce the boilerplate in the most complex cases.
Handle
A handle is a thin wrapper around an entity and a registry. It replicates the
API of a registry by offering functions such as get or emplace. The
difference being that the entity is implicitly passed to the registry.
It is default constructible as an invalid handle that contains a null registry
and a null entity. When it contains a null registry, calling functions that
delegate execution to the registry causes undefined behavior. It is recommended
to test for validity with its implicit cast to bool if in doubt.
A handle is also non-owning, meaning that it is freely copied and moved around
without affecting its entity (in fact, handles happen to be trivially copyable).
An implication of this is that mutability becomes part of the type.
There are two aliases that use entt::entity as their default entity:entt::handle and entt::const_handle.
Users can also easily create their own aliases for custom identifiers as:
using my_handle = entt::basic_handle<entt::basic_registry<my_identifier>>;
using my_const_handle = entt::basic_handle<const entt::basic_registry<my_identifier>>;Non-const handles are also implicitly convertible to const handles out of the
box but not the other way around.
This class is intended to simplify function signatures. In case of functions
that take a registry and an entity and do most of their work on that entity,
users might want to consider using handles, either const or non-const.
Organizer
The organizer class template offers support for creating an execution graph
from a set of functions and their requirements on resources.
The resulting tasks are not executed in any case. This is not the goal of this
tool. Instead, they are returned to the user in the form of a graph that allows
for safe execution.
All functions are added in order of execution to the organizer:
entt::organizer organizer;
// adds a free function to the organizer
organizer.emplace<&free_function>();
// adds a member function and an instance on which to invoke it to the organizer
clazz instance;
organizer.emplace<&clazz::member_function>(&instance);
// adds a decayed lambda directly
organizer.emplace(+[](const void *, entt::registry &) { /* ... */ });These are the parameters that a free function or a member function can accept:
- A possibly constant reference to a registry.
- An
entt::basic_viewwith any possible combination of storage classes. - A possibly constant reference to any type
T(that is, a context variable).
The function type for free functions and decayed lambdas passed as parameters toemplace is void(const void *, entt::registry &) instead. The first parameter
is an optional pointer to user defined data to provide upon registration:
clazz instance;
organizer.emplace(+[](const void *, entt::registry &) { /* ... */ }, &instance);In all cases, it is also possible to associate a name with the task when
creating it. For example:
organizer.emplace<&free_function>("func");When a function is registered with the organizer, everything it accesses is
considered a resource (views are unpacked and their types are treated as
resources). The constness of a type also dictates its access mode (RO/RW). In
turn, this affects the resulting graph, since it influences the possibility of
launching tasks in parallel.
As for the registry, if a function does not explicitly request it or requires a
constant reference to it, it is considered a read-only access. Otherwise, it is
considered as read-write access. All functions have the registry among their
resources.
When registering a function, users can also require resources that are not in
the list of parameters of the function itself. These are declared as template
parameters:
organizer.emplace<&free_function, position, velocity>("func");Similarly, users can override the access mode of a type again via template
parameters:
organizer.emplace<&free_function, const renderable>("func");In this case, even if renderable appears among the parameters of the function
as not constant, it is treated as constant as regards the generation of the task
graph.
To generate the task graph, the organizer offers the graph member function:
std::vector<entt::organizer::vertex> graph = organizer.graph();A graph is returned in the form of an adjacency list. Each vertex offers the
following features:
ro_countandrw_count: the number of resources accessed in read-only or
read-write mode.ro_dependencyandrw_dependency: type info objects associated with the
parameters of the underlying function.top_level: true if a node is a top level one (it has no entering edges),
false otherwise.info: type info object associated with the underlying function.name: the name associated with the given vertex if any, a null pointer
otherwise.callback: a pointer to the function to execute and whose function type isvoid(const void *, entt::registry &).data: optional data to provide to the callback.children: the vertices reachable from the given node, in the form of indices
within the adjacency list.
Since the creation of pools and resources within the registry is not necessarily
thread safe, each vertex also offers a prepare function which is used to setup
a registry for execution with the created graph:
auto graph = organizer.graph();
entt::registry registry;
for(auto &&node: graph) {
node.prepare(registry);
}The actual scheduling of the tasks is the responsibility of the user, who can
use the preferred tool.
Context variables
Each registry has a context associated with it, which is an any object map
accessible by both type and name for convenience. The name is not really a
name though. In fact, it is a numeric id of type id_type used as a key for the
variable. Any value is accepted, even runtime ones.
The context is returned via the ctx functions and offers a minimal set of
features including the following:
// creates a new context variable by type and returns it
registry.ctx().emplace<my_type>(42, 'c');
// creates a new named context variable by type and returns it
registry.ctx().emplace_as<my_type>("my_variable"_hs, 42, 'c');
// inserts or assigns a context variable by (deduced) type and returns it
registry.ctx().insert_or_assign(my_type{42, 'c'});
// inserts or assigns a named context variable by (deduced) type and returns it
registry.ctx().insert_or_assign("my_variable"_hs, my_type{42, 'c'});
// gets the context variable by type as a non-const reference from a non-const registry
auto &var = registry.ctx().get<my_type>();
// gets the context variable by name as a const reference from either a const or a non-const registry
const auto &cvar = registry.ctx().get<const my_type>("my_variable"_hs);
// resets the context variable by type
registry.ctx().erase<my_type>();
// resets the context variable associated with the given name
registry.ctx().erase<my_type>("my_variable"_hs);There are no strict requirements on the type of a context variable, such as that
it must be constructible or movable by default. However, if the supplied type
does not match that of the variable when using a name, the operation
fails.
For all users who want to use the context but do not want to create elements,
the contains and find functions are also available:
const bool contains = registry.ctx().contains<my_type>();
const my_type *value = registry.ctx().find<const my_type>("my_variable"_hs);Also in this case, both functions support constant types and accept a name for
the variable to look up, as does at.
Aliased properties
A context also supports creating aliases for existing variables that are not
directly managed by the registry. Const and therefore read-only variables are
also accepted.
To do that, the type used upon construction must be a reference type and an
lvalue is necessarily provided as an argument:
time clock;
registry.ctx().emplace<time &>(clock);Read-only aliased properties are created using const types instead:
registry.ctx().emplace<const time &>(clock);Note that insert_or_assign does not support aliased properties and users must
necessarily use emplace or emplace_as for this purpose.
When insert_or_assign is used to update an aliased property, it converts
the property itself into a non-aliased one.
From the point of view of the user, there are no differences between a variable
that is managed by the registry and an aliased property. However, read-only
variables are not accessible as non-const references:
// read-only variables only support const access
const my_type *ptr = registry.ctx().find<const my_type>();
const my_type &var = registry.ctx().get<const my_type>();Aliased properties are erased as it happens with any other variable. Similarly,
it is also possible to assign them a name.
Snapshot: complete vs continuous
This module comes with bare minimum support for serialization.
It does not convert components to bytes directly, there was not the need of
another tool for serialization out there. Instead, it accepts an opaque object
with a suitable interface (namely an archive) to serialize its internal data
structures and restore them later. The way types and instances are converted to
a bunch of bytes is completely in charge to the archive and thus to final users.
The goal of the serialization part is to allow users to make both a dump of the
entire registry or a narrower snapshot, that is to select only the components in
which they are interested.
Intuitively, the use cases are different. As an example, the first approach is
suitable for local save/restore functionalities while the latter is suitable for
creating client-server applications and for transferring somehow parts of the
representation side to side.
To take a snapshot of a registry, use the snapshot class:
output_archive output;
entt::snapshot{registry}
.get<entt::entity>(output)
.get<a_component>(output)
.get<another_component>(output);It is not necessary to invoke all functions each and every time. What functions
to use in which case mostly depends on the goal.
When getting an entity type, the snapshot class serializes all entities along
with their versions.
In all other cases, entities and components from a given storage are passed to
the archive. Named pools are also supported:
entt::snapshot{registry}.get<a_component>(output, "other"_hs);There exists another version of the get member function that accepts a range
of entities to serialize. It can be used to filter out those entities that
should not be serialized for some reasons:
const auto view = registry.view<serialize>();
output_archive output;
entt::snapshot{registry}
.get<a_component>(output, view.begin(), view.end())
.get<another_component>(output, view.begin(), view.end());Once a snapshot is created, there exist mainly two ways to load it: as a whole
and in a kind of continuous mode.
The following sections describe both loaders and archives in details.
Snapshot loader
A snapshot loader requires that the destination registry be empty. It loads all
the data at once while keeping intact the identifiers that the entities
originally had:
input_archive input;
entt::snapshot_loader{registry}
.get<entt::entity>(input)
.get<a_component>(input)
.get<another_component>(input)
.orphans();It is not necessary to invoke all functions each and every time. What functions
to use in which case mostly depends on the goal.
For obvious reasons, what is important is that the data are restored in exactly
the same order in which they were serialized.
When getting an entity type, a snapshot loader restores all entities with the
versions that they originally had at the source.
In all other cases, entities and components are restored in a given storage. If
the registry does not contain the entity, it is also created accordingly. As for
the snapshot class, named pools are supported too:
entt::snapshot_loader{registry}.get<a_component>(input, "other"_hs);Finally, the orphans member function releases the entities that have no
components after a restore, if any.
Continuous loader
A continuous loader is designed to load data from a source registry to a
(possibly) non-empty destination. The loader accommodates in a registry more
than one snapshot in a sort of continuous loading that updates the destination
one step at a time.
Identifiers that entities originally had are not transferred to the target.
Instead, the loader maps remote identifiers to local ones while restoring a
snapshot. Wrapping the archive is a conveninent way of updating identifiers that
are part of components automatically (see the example below).
Another difference with the snapshot loader is that the continuous loader has an
internal state that must persist over time. Therefore, there is no reason to
limit its lifetime to that of a temporary object:
entt::continuous_loader loader{registry};
input_archive input;
auto archive = [&loader, &input](auto &value) {
input(value);
if constexpr(std::is_same_v<std::remove_reference_t<decltype(value)>, dirty_component>) {
value.parent = loader.map(value.parent);
value.child = loader.map(value.child);
}
};
loader
.get<entt::entity>(input)
.get<a_component>(input)
.get<another_component>(input)
.get<dirty_component>(input)
.orphans();It is not necessary to invoke all functions each and every time. What functions
to use in which case mostly depends on the goal.
For obvious reasons, what is important is that the data are restored in exactly
the same order in which they were serialized.
When getting an entity type, a loader restores groups of entities and maps
each entity to a local counterpart when required. For each remote identifier not
yet registered by the loader, a local identifier is created so as to keep the
local entity in sync with the remote one.
In all other cases, entities and components are restored in a given storage. If
the registry does not contain the entity, it is also tracked accordingly. As for
the snapshot class, named pools are supported too:
loader.get<a_component>(input, "other"_hs);Finally, the orphans member function releases the entities that have no
components after a restore, if any.
Archives
Archives must publicly expose a predefined set of member functions. The API is
straightforward and consists only of a group of function call operators that
are invoked by the snapshot class and the loaders.
In particular:
An output archive (the one used when creating a snapshot) exposes a function
call operator with the following signature to store entities:cppvoid operator()(entt::entity);Where
entt::entityis the type of the entities used by the registry.
Note that all member functions of the snapshot class also make an initial call
to store aside the size of the set they are going to store. In this case,
the expected function type for the function call operator is:cppvoid operator()(std::underlying_type_t<entt::entity>);In addition, an archive accepts (const) references to the types of component
to serialize. Therefore, given a typeT, the archive offers a function call
operator with the following signature:cppvoid operator()(const T &);The output archive can freely decide how to serialize the data. The registry
is not affected at all by the decision.An input archive (the one used when restoring a snapshot) exposes a function
call operator with the following signature to load entities:cppvoid operator()(entt::entity &);Where
entt::entityis the type of the entities used by the registry. Each
time the function is invoked, the archive reads the next element from the
underlying storage and copies it in the given variable.
All member functions of a loader class also make an initial call to read the
size of the set they are going to load. In this case, the expected function
type for the function call operator is:cppvoid operator()(std::underlying_type_t<entt::entity> &);In addition, an archive accepts references to the types of component to
restore. Therefore, given a typeT, the archive contains a function call
operator with the following signature:cppvoid operator()(T &);Every time this operator is invoked, the archive reads the next element from
the underlying storage and copies it in the given variable.
One example to rule them all
EnTT comes with some examples (actually some tests) that show how to integrate
a well known library for serialization as an archive. It usesCereal C++ under the hood, mainly
because I wanted to learn how it works at the time I was writing the code.
The code is not production-ready and it is not neither the only nor
(probably) the best way to do it. However, feel free to use it at your own
risk.
The basic idea is to store everything in a group of queues in memory, then bring
everything back to the registry with different loaders.
Storage
Pools of components are specialized versions of the sparse set class. Each
pool contains all the instances of a single component type and all the entities
to which it is assigned.
Sparse arrays are paged to avoid wasting memory. Packed arrays of components
are also paged to have pointer stability upon additions. Packed arrays of
entities are not instead.
All pools rearranges their items in order to keep the internal arrays tightly
packed and maximize performance, unless full pointer stability is enabled.
Component traits
In EnTT, almost everything is customizable. Pools are no exception.
In this case, the standardized way to access all component properties is thecomponent_traits class.
Various parts of the library access component properties through this class. It
makes it possible to use any type as a component, as long as its specialization
of component_traits implements all the required functionalities.
The non-specialized version of this class contains the following members:
in_place_delete:Type::in_place_deleteif present, true for non-movable
types and false otherwise.page_size:Type::page_sizeif present,ENTT_PACKED_PAGEfor non-empty
types and 0 otherwise.
Where Type is any type of component. Properties are customized by specializing
the above class and defining its members, or by adding only those of interest to
a component definition:
struct transform {
static constexpr auto in_place_delete = true;
// ... other data members ...
};The component_traits class template takes care of extracting the properties
from the supplied type.
Plus, it can be specialized and constrained with a concept to further customize
it on a per type or per feature basis.
Empty type optimization
An empty type T is such that std::is_empty_v<T> returns true. They also are
the same types for which empty base optimization (EBO) is possible.EnTT handles these types in a special way, optimizing both in terms of
performance and memory usage. However, this also has consequences that are worth
mentioning.
When an empty type is detected, it is not instantiated by default. Therefore,
only the entities to which it is assigned are made available. There does not
exist a way to get empty types from a storage or a registry. Views and groups
never return their instances too (for example, during a call to each).
On the other hand, iterations are faster because only the entities to which the
type is assigned are considered. Moreover, less memory is used, mainly because
there does not exist any instance of the component, no matter how many entities
it is assigned to.
More in general, none of the feature offered by the library is affected, but for
the ones that require to return actual instances.
This optimization is disabled by defining the ENTT_NO_ETO macro. In this case,
empty types are treated like all other types. Setting a page size at component
level via the component_traits class template is another way to disable this
optimization selectively rather than globally.
Void storage
A void storage (entt::storage<void> or entt::basic_storage<void, Entity>),
is a fully functional storage type used to create pools not associated with a
particular component type.
From a technical point of view, it is in all respects similar to a storage for
empty types when their optimization is enabled. Pagination is disabled as well
as pointer stability (as not necessary).
However, this should be preferred to using a simple sparse set. In particular,
a void storage offers all those feature normally offered by other storage types.
Therefore, it is a perfectly valid pool for use with views and groups or within
a registry.
Entity storage
This storage is such that the component type is the same as the entity type, for
example entt::storage<entt::entity> or entt::basic_storage<Type, Type>.
For this type of pools, there is a specific specialization within EnTT. In
fact, entities are subject to different rules with respect to components
(although still customizable by the user if needed). In particular:
Entities are never truly deleted. They are moved out of the list of entities
in use and their versions are updated automatically.There are no
emplaceorinsertfunctions in its interface. Instead, a
range ofgeneratefunctions are provided for creating or recycling entities.The
eachfunction returns an iterable object to visit the entities in use,
that is, those not marked as ready for reuse. To iterate all the entities it
is necessary to iterate the underlying sparse set instead.
This kind of storage is designed to be used where any other storage is fine and
can therefore be combined with views, groups and so on.
Reserved identifiers
Since the entity storage is the one in charge of generating identifiers, it is
also possible to request that some of them be reserved and never returned.
By doing so, users can then generate and manage them autonomously, as needed.
To set a starting identifier, the start_from function is invoked as follows:
storage.start_from(entt::entity{100});Note that the version is irrelevant and is ignored in all cases. Identifiers are
always generated with default version.
By calling start_from as above, the first 100 elements are discarded and the
first identifier returned is the one with entity 100 and version 0.
One of a kind to the registry
Within the registry, an entity storage is treated in all respects like any other
storage.
Therefore, it is possible to add mixins to it as well as retrieve it via thestorage function. It can also be used as storage in a view (for exclude-only
views for example):
auto view = registry.view<entt::entity>(entt::exclude<my_type>);However, it is also subject to a couple of exceptions, partly out of necessity
and partly for ease of use.
In particular, it is not possible to create multiple elements of this type.
This means that the name used to retrieve this kind of storage is ignored and
the registry will only ever return the same element to the caller. For example:
auto &other = registry.storage<entt::entity>("other"_hs);In this case, the identifier is discarded as is. The call is in all respects
equivalent to the following:
auto &storage = registry.storage<entt::entity>();Because entity storage does not have a name, it cannot be retrieved via the
opaque storage function either.
It would make no sense to try anyway, given that the type of the registry and
therefore its entity type are known regardless.
Finally, when the user asks the registry for an iterable object to visit all the
storage elements inside it as follows:
for(auto [id, storage]: registry.storage()) {
// ...
}Entity storage is never returned. This simplifies many tasks (such as copying an
entity) and fits perfectly with the fact that this type of storage does not have
an identifier inside the registry.
Pointer stability
The ability to achieve pointer stability for one, several or all components is a
direct consequence of the design of EnTT and of its default storage.
In fact, although it contains what is commonly referred to as a packed array,
the default storage is paged and does not suffer from invalidation of references
when it runs out of space and has to reallocate.
However, this is not enough to ensure pointer stability in case of deletion. For
this reason, a stable deletion method is also offered. This one is such that
the position of the elements is preserved by creating tombstones upon deletion
rather than trying to fill the holes that are created.
For performance reasons, EnTT favors storage compaction in all cases, although
often accessing a component occurs mostly randomly or traversing pools in a
non-linear order on the user side (as in the case of a hierarchy).
In other words, pointer stability is not automatic but is enabled on request.
In-place delete
The library offers out of the box support for in-place deletion, thus offering
storage with completely stable pointers. This is achieved by specializing thecomponent_traits class or by adding the required properties to the component
definition when needed.
Views and groups adapt accordingly when they detect a storage with a different
deletion policy than the default. In particular:
- Groups are incompatible with stable storage and even refuse to compile.
- Multi type and runtime views are completely transparent to storage policies.
- Single type views for stable storage types offer the same interface of multi
type views. For example, onlysize_hintis available.
In other words, the more generic version of a view is provided in case of stable
storage, even for a single type view.
In no case a tombstone is returned from the view itself. Likewise, non-existent
components are not returned, which could otherwise result in an UB.
Hierarchies and the like
EnTT does not attempt in any way to offer built-in methods with hidden or
unclear costs to facilitate the creation of hierarchies.
There are various solutions to the problem, such as using the following class:
struct relationship {
std::size_t children{};
entt::entity first{entt::null};
entt::entity prev{entt::null};
entt::entity next{entt::null};
entt::entity parent{entt::null};
// ... other data members ...
};However, it should be pointed out that the possibility of having stable pointers
for one, many or all types solves the problem of hierarchies at the root in many
cases.
In fact, if a certain type of component is visited mainly in random order or
according to hierarchical relationships, using direct pointers has many
advantages:
struct transform {
static constexpr auto in_place_delete = true;
transform *parent;
// ... other data members ...
};Furthermore, it is quite common for a group of elements to be created close in
time and therefore fallback into adjacent positions, thus favoring locality even
on random accesses. Locality that is not sacrificed over time given the
stability of storage positions, with undoubted performance advantages.
Meet the runtime
EnTT takes advantage of what the language offers at compile-time. However,
this can have its downsides (well known to those familiar with type erasure
techniques).
To fill the gap, the library also provides a bunch of utilities and features
that are invaluable to handle types and pools at runtime.
A base class to rule them all
Storage classes are fully self-contained types. They are extended via mixins
to add more functionalities (generic or type specific). In addition, they offer
a basic set of functions that already allow users to go very far.
The aim is to limit the need for customizations as much as possible, offering
what is usually necessary for the vast majority of cases.
When a storage is used through its base class (for example, when its actual type
is not known), there is always the possibility of receiving a type_info object
for the type of elements associated with the entities (if any):
if(entt::type_id<velocity>() == base.info()) {
// ...
}Furthermore, all features rely on internal functions that forward the calls to
the mixins. The latter can then make use of any information, which is set viabind:
base.bind(registry);The bind function accepts any element by reference or by value and forwards it
to derived classes.
This is how a registry passes itself to all pools that support signals and
also why a storage keeps sending events without requiring the registry to be
passed to it every time.
Alongside these more specific things, there are also a couple of functions
designed to address some common requirements such as copying an entity.
In particular, the base class behind a storage offers the possibility to take
the value associated with an entity through an opaque pointer:
const void *instance = base.value(entity);Similarly, the non-specialized push function accepts an optional opaque
pointer and behaves differently depending on the case:
When the pointer is null, the function tries to default-construct an instance
of the object to bind to the entity and returns true on success.When the pointer is not null, the function tries to copy-construct an instance
of the object to bind to the entity and returns true on success.
This means that, starting from a reference to the base, it is possible to bind
components with entities without knowing their actual type and even initialize
them by copy if needed:
// create a copy of an entity component by component
for(auto &&curr: registry.storage()) {
if(auto &storage = curr.second; storage.contains(src)) {
storage.push(dst, storage.value(src));
}
}This is particularly useful to clone entities in an opaque way. In addition, the
decoupling of features allows for filtering or use of different copying policies
depending on the type.
Beam me up, registry
EnTT allows the user to assign a name (or rather, a numeric identifier) to a
type and then create multiple pools of the same type:
using namespace entt::literals;
auto &&storage = registry.storage<velocity>("second pool"_hs);If a name is not provided, the default storage associated with the given type is
always returned.
Since the storage are also self-contained, the registry does not duplicate its
own API for them. However, there is still no limit to the possibilities of use:
auto &&other = registry.storage<velocity>("other"_hs);
registry.emplace<velocity>(entity);
other.push(entity);Anything that can be done via the registry interface can also be done directly
on the reference storage.
On the other hand, those calls involving all storage are guaranteed to also
reach manually created ones:
// removes the entity from both storage
registry.destroy(entity);Finally, a storage of this type works with any view (which also accepts multiple
storages of the same type, if necessary):
// direct initialization
entt::basic_view direct{
registry.storage<velocity>(),
registry.storage<velocity>("other"_hs)
};
// concatenation
auto join = registry.view<velocity>() | entt::basic_view{registry.storage<velocity>("other"_hs)};The possibility of direct use of storage combined with the freedom of being able
to create and use more than one of the same type opens the door to the use ofEnTT at runtime, which was previously quite limited.
Views and Groups
Views are a non-intrusive tool for working with entities and components without
affecting other functionalities or increasing memory consumption.
Groups are an intrusive tool to use to improve performance along critical paths
but which also has a price to pay for that.
There are mainly two kinds of views: compile-time (also known as view) and
runtime (also known as runtime_view).
The former requires a compile-time list of component (or storage) types and can
make several optimizations because of that. The latter is constructed at runtime
using numerical type identifiers instead and is a bit slower to iterate.
In both cases, creating and destroying views is not expensive at all since they
do not have any type of initialization.
Groups come in three different flavors: full-owning groups, partial-owning
groups and non-owning groups. The main difference between them is in terms of
performance.
Groups can literally own one or more component types. They are allowed to
rearrange pools so as to speed up iterations. Roughly speaking: the more
components a group owns, the faster it is to iterate them.
Views
Single type views and multi type views behave differently and also have slightly
different APIs.
Single type views are specialized to give a performance boost in all cases.
There is nothing as fast as a single type view. They just walk through packed
(actually paged) arrays of elements and return them directly.
This kind of views also allows getting the exact number of elements they are
going to return.
Refer to the inline documentation for all the details.
Multi type views iterate entities that have at least all the given components.
During construction, they look at the number of elements available in each pool
and use the smallest set in order to speed up iterations.
This kind of views only allow to get the estimated number of elements they are
going to return.
Refer to the inline documentation for all the details.
Storing aside views is not required as they are extremely cheap to construct. In
fact, this is even discouraged when creating a view from a const registry. Since
all storage are lazily initialized, they may not exist when the view is created.
Thus, while perfectly usable, the view may contain pending references that are
never reinitialized with the actual storage.
Views share the way they are created by means of a registry:
// single type view
auto single = registry.view<position>();
// multi type view
auto multi = registry.view<position, velocity>();Filtering entities by components is also supported:
auto view = registry.view<position, velocity>(entt::exclude<renderable>);To iterate a view, either use it in a range-for loop:
auto view = registry.view<position, velocity, renderable>();
for(auto entity: view) {
// a component at a time ...
auto &position = view.get<position>(entity);
auto &velocity = view.get<velocity>(entity);
// ... multiple components ...
auto [pos, vel] = view.get<position, velocity>(entity);
// ... all components at once
auto [pos, vel, rend] = view.get(entity);
// ...
}Or rely on the each member functions to iterate both entities and components
at once:
// through a callback
registry.view<position, velocity>().each([](auto entity, auto &pos, auto &vel) {
// ...
});
// using an input iterator
for(auto &&[entity, pos, vel]: registry.view<position, velocity>().each()) {
// ...
}Note that entities can also be excluded from the parameter list when received
through a callback and this can improve even further the performance during
iterations.
Since they are not explicitly instantiated, empty components are not returned in
any case.
As a side note, in the case of single type views, get accepts but does not
strictly require a template parameter, since the type is implicitly defined.
However, when the type is not specified, the instance is returned using a tuple
for consistency with multi type views:
auto view = registry.view<const renderable>();
for(auto entity: view) {
auto [renderable] = view.get(entity);
// ...
}Note: prefer the get member function of a view instead of that of a
registry during iterations to get the types iterated by the view itself.
Create once, reuse many times
Views support lazy initialization as well as storage swapping.
An empty (or partially initialized) view is such that it returns false when
converted to bool (to let the user know that it is not fully initialized) but it
also works as-is like any other view.
In order to initialize a view one piece at a time, it allows users to inject
storage classes when available:
entt::storage_for_t<velocity> storage{};
entt::view<entt::get_t<position, velocity>> view{};
view.storage(storage);If there are multiple storages of the same type, it is possible to disambiguate
using the index of the element to be replaced:
view.storage<1>(storage);The ability to literally replace a storage in a view also opens up its reuse
with different sets of entities.
For example, to filter a view based on two groups of entities with different
characteristics, there will be no need to reinitialize anything:
entt::view<entt::get<my_type, void>> view{registry.storage<my_type>>()};
entt::storage_for_t<void> the_good{};
entt::storage_for_t<void> the_bad{};
// initialize the sets above as needed
view.storage(the_good);
for(auto [entt, elem]: view) {
// the good entities with their components here
}
view.storage(the_bad);
for(auto [entt, elem]: view) {
// the bad entities with their components here
}Finally, it should be noted that the lack of a storage is treated to all intents
and purposes as if it were an empty element.
Thus, a get storage (as in entt::get_t) makes the view empty automatically
while an exclude storage (as in entt::exclude_t) is ignored as if that part
of the filter did not exist.
Exclude-only
Exclude-only views are not really a thing in EnTT.
However, the same result can be achieved by combining the right storage into a
simple view.
If one gets to the root of the problem, the purpose of an exclude-only view is
to return entities that do not meet certain requirements.
Since entity storage, unlike exclude-only views, is a thing in EnTT, users
can leverage it for these kinds of queries. It is also guaranteed to be unique
within a registry and is always accessible when creating a view:
auto view = registry.view<entt::entity>(entt::exclude<my_type>);The returned view is such that it will return only the entities that do not have
the my_type component, regardless of what other components they have.
View pack
Views are combined with storage objects and with each other to create new, more
specific queries.
The type returned when combining multiple elements together is itself a view,
more in general a multi component one.
Combining different elements tries to mimic ranges:
auto view = registry.view<position>();
auto other = registry.view<velocity>();
const auto &storage = registry.storage<renderable>();
auto pack = view | other | renderable;The constness of the types is preserved and their order depends on the order in
which the views are combined. For example, the above pack first returns an
instance of position, then one of velocity, and finally one ofrenderable.
Since combining elements generates views, a chain can be of arbitrary length and
the above type order rules apply sequentially.
Iteration order
By default, a view is iterated along the pool that contains the smallest number
of elements.
For example, if the registry contains fewer velocitys than it containspositions, then the order of the elements returned by the following view
depends on how the velocity components are arranged in their pool:
for(auto entity: registry.view<positon, velocity>()) {
// ...
}Moreover, the order of types when constructing a view does not matter. Neither
does the order of views in a view pack.
However, it is possible to enforce iteration of a view by given component
order by means of the use function:
auto view = registry.view<position, velocity>();
view.use<position>();
for(auto entity: view) {
// ...
}On the other hand, if all a user wants is to iterate the elements in reverse
order, this is possible for a single type view using its reverse iterators:
auto view = registry.view<position>();
for(auto it = view.rbegin(), last = view.rend(); it != last; ++iter) {
// ...
}Unfortunately, multi type views do not offer reverse iterators. Therefore, in
this case it is a must to implement this functionality manually or to use single
type views to lead the iteration.
Runtime views
Multi type views iterate entities that have at least all the given components.
During construction, they look at the number of elements available in each pool
and use the smallest set in order to speed up iterations.
They offer more or less the same functionalities of a multi type view. However,
they do not expose a get member function and users should refer to the
registry that generated the view to access components.
Refer to the inline documentation for all the details.
Runtime views are pretty cheap to construct and should not be stored aside in
any case. They should be used immediately after creation and then they should be
thrown away.
To iterate a runtime view, either use it in a range-for loop:
entt::runtime_view view{};
view.iterate(registry.storage<position>()).iterate(registry.storage<velocity>());
for(auto entity: view) {
// ...
}Or rely on the each member function to iterate entities:
entt::runtime_view{}
.iterate(registry.storage<position>())
.iterate(registry.storage<velocity>())
.each([](auto entity) {
// ...
});Performance are exactly the same in both cases.
Filtering entities by components is also supported for this kind of views:
entt::runtime_view view{};
view.iterate(registry.storage<position>()).exclude(registry.storage<velocity>());Runtime views are meant for when users do not know at compile-time what types to
use to iterate entities. The storage member function of a registry could be
useful in this regard.
Groups
Groups are meant to iterate multiple components at once and to offer a faster
alternative to multi type views.
Groups overcome the performance of the other tools available but require to get
the ownership of components. This sets some constraints on their pools. On the
other hand, groups are not an automatism that increases memory consumption,
affects functionalities and tries to optimize iterations for all the possible
combinations of components. Users can decide when to pay for groups and to what
extent.
The most interesting aspect of groups is that they fit usage patterns. Other
solutions around usually try to optimize everything, because it is known that
somewhere within the everything there are also our usage patterns. However
this has a cost that is not negligible, both in terms of performance and memory
usage. Ironically, users pay the price also for things they do not want and this
is not something I like much. Even worse, one cannot easily disable such a
behavior. Groups work differently instead and are designed to optimize only the
real use cases when users find they need to.
Another nice-to-have feature of groups is that they have no impact on memory
consumption, put aside full non-owning groups that are pretty rare and should be
avoided as long as possible.
All groups affect to an extent the creation and destruction of their components.
This is due to the fact that they must observe changes in the pools of
interest and arrange data correctly when needed for the types they own.
In all cases, a group allows to get the exact number of elements it is going to
return.
Refer to the inline documentation for all the details.
Storing aside groups is not required as they are extremely cheap to create, even
though valid groups can be copied without problems and reused freely.
A group performs an initialization step the very first time it is requested and
this could be quite costly. To avoid it, consider creating the group when no
components have been assigned yet. If the registry is empty, preparation is
extremely fast.
To iterate a group, either use it in a range-for loop:
auto group = registry.group<position>(entt::get<velocity, renderable>);
for(auto entity: group) {
// a component at a time ...
auto &position = group.get<position>(entity);
auto &velocity = group.get<velocity>(entity);
// ... multiple components ...
auto [pos, vel] = group.get<position, velocity>(entity);
// ... all components at once
auto [pos, vel, rend] = group.get(entity);
// ...
}Or rely on the each member functions to iterate both entities and components
at once:
// through a callback
registry.group<position>(entt::get<velocity>).each([](auto entity, auto &pos, auto &vel) {
// ...
});
// using an input iterator
for(auto &&[entity, pos, vel]: registry.group<position>(entt::get<velocity>).each()) {
// ...
}Note that entities can also be excluded from the parameter list when received
through a callback and this can improve even further the performance during
iterations.
Since they are not explicitly instantiated, empty components are not returned in
any case.
Note: prefer the get member function of a group instead of that of a
registry during iterations to get the types iterated by the group itself.
Full-owning groups
A full-owning group is the fastest tool a user can expect to use to iterate
multiple components at once. It iterates all the components directly, no
indirection required.
This type of groups performs more or less as if users are accessing sequentially
a bunch of packed arrays of components all sorted identically, with no jumps nor
branches.
A full-owning group is created as:
auto group = registry.group<position, velocity>();Filtering entities by components is also supported:
auto group = registry.group<position, velocity>({}, entt::exclude<renderable>);Once created, the group gets the ownership of all the components specified in
the template parameter list and arranges their pools as needed.
Sorting owned components is no longer allowed once the group has been created.
However, full-owning groups are sorted using their sort member functions.
Sorting a full-owning group affects all its instances.
Partial-owning groups
A partial-owning group works similarly to a full-owning group for the components
it owns, but relies on indirection to get components owned by other groups.
This is not as fast as a full-owning group, but it is already much faster than a
view when there are only one or two free components to retrieve (the most common
cases likely). In the worst case, it is not slower than views anyway.
A partial-owning group is created as:
auto group = registry.group<position>(entt::get<velocity>);Filtering entities by components is also supported:
auto group = registry.group<position>(entt::get<velocity>, entt::exclude<renderable>);Once created, the group gets the ownership of all the components specified in
the template parameter list and arranges their pools as needed. The ownership of
the types provided via entt::get does not pass to the group instead.
Sorting owned components is no longer allowed once the group has been created.
However, partial-owning groups are sorted using their sort member functions.
Sorting a partial-owning group affects all its instances.
Non-owning groups
Non-owning groups are usually fast enough, for sure faster than views and well
suited for most of the cases. However, they require custom data structures to
work properly and they increase memory consumption.
As a rule of thumb, users should avoid using non-owning groups, if possible.
A non-owning group is created as:
auto group = registry.group<>(entt::get<position, velocity>);Filtering entities by components is also supported:
auto group = registry.group<>(entt::get<position, velocity>, entt::exclude<renderable>);The group does not receive the ownership of any type of component in this
case. This type of groups is therefore the least performing in general, but also
the only one that can be used in any situation to slightly improve performance.
Non-owning groups are sorted using their sort member functions. Sorting a
non-owning group affects all its instances.
Types: const, non-const and all in between
The registry class offers two overloads when it comes to constructing views
and groups: a const version and a non-const one. The former accepts only const
types as template parameters, the latter accepts both const and non-const types
instead.
It means that views and groups generated by a const registry also propagate the
constness to the types involved. As an example:
entt::view<entt::get_t<const position, const velocity>> view = std::as_const(registry).view<const position, const velocity>();Consider the following definition for a non-const view instead:
entt::view<entt::get_t<position, const velocity>> view = registry.view<position, const velocity>();In the example above, view is used to access either read-only or writableposition components while velocity components are read-only in all
cases.
Similarly, these statements are all valid:
position &pos = view.get<position>(entity);
const position &cpos = view.get<const position>(entity);
const velocity &cpos = view.get<const velocity>(entity);
std::tuple<position &, const velocity &> tup = view.get<position, const velocity>(entity);
std::tuple<const position &, const velocity &> ctup = view.get<const position, const velocity>(entity);It is not possible to get non-const references to velocity components from the
same view instead. Therefore, these result in compilation errors:
velocity &cpos = view.get<velocity>(entity);
std::tuple<position &, velocity &> tup = view.get<position, velocity>(entity);
std::tuple<const position &, velocity &> ctup = view.get<const position, velocity>(entity);The each member functions also propagates constness to its return values:
view.each([](auto entity, position &pos, const velocity &vel) {
// ...
});A caller can still refer to the position components through a const reference
because of the rules of the language that fortunately already allow it.
The same concepts apply to groups as well.
Give me everything
Views and groups are narrow windows on the entire list of entities. They work by
filtering entities according to their components.
In some cases there may be the need to iterate all the entities still in use
regardless of their components. This is done by accessing entity storage:
for(auto entity: registry.view<entt::entity>()) {
// ...
}As a rule of thumb, consider using a view or a group if the goal is to iterate
entities that have a determinate set of components. These tools are usually much
faster than filtering entities with a bunch of custom tests.
In all the other cases, this is the way to go. For example, it is possible to
combine this view with the orphan member function to clean up orphan entities
(that is, entities that are still in use and have no assigned components):
for(auto entity: registry.view<entt::entity>()) {
if(registry.orphan(entity)) {
registry.release(entity);
}
}In general, iterating all entities can result in poor performance. It should not
be done frequently to avoid the risk of a performance hit.
However, it is convenient when initializing an editor or to reclaim pending
identifiers.
What is allowed and what is not
Most of the ECS available out there do not allow to create and destroy
entities and components during iterations, nor to have pointer stability.EnTT partially solves the problem with a few limitations:
Creating entities and components is allowed during iterations in most cases
and it never invalidates already existing references.Deleting the current entity or removing its components is allowed during
iterations but it could invalidate references. For all the other entities,
destroying them or removing their iterated components is not allowed and can
result in undefined behavior.When pointer stability is enabled for the type leading the iteration, adding
instances of the same type may or may not cause the entity involved to be
returned. Destroying entities and components is always allowed instead, even
if not currently iterated, without the risk of invalidating any references.In case of reverse iterations, adding or removing elements is not allowed
under any circumstances. It could quickly lead to undefined behaviors.
In other terms, iterators are rarely invalidated. Also, component references
are not invalidated when a new element is added while they could be invalidated
upon destruction due to the swap-and-pop policy, unless the type leading the
iteration undergoes in-place deletion.
As an example, consider the following snippet:
registry.view<position>().each([&](const auto entity, auto &pos) {
registry.emplace<position>(registry.create(), 0., 0.);
// references remain stable after adding new instances
pos.x = 0.;
});The each member function will not break (because iterators remain valid) nor
will any reference be invalidated. Instead, more attention should be paid to the
destruction of entities or the removal of components.
Use a common range-for loop and get components directly from the view or move
the deletion of entities and components at the end of the function to avoid
dangling pointers.
For all types that do not offer stable pointers, iterators are also invalidated
and the behavior is undefined if an entity is modified or destroyed and it is
not the one currently returned by the iterator nor a newly created one.
To work around it, possible approaches are:
Store aside the entities and the components to be removed and perform the
operations at the end of the iteration.Mark entities and components with a proper tag component that indicates they
must be purged, then perform a second iteration to clean them up one by one.
A notable side effect of this feature is that the number of required allocations
is further reduced in most cases.
More performance, more constraints
Groups are a faster alternative to views. However, the higher the performance,
the greater the constraints on what is allowed and what is not.
In particular, groups add in some rare cases a limitation on the creation of
components during iterations. It happens in quite particular cases. Given the
nature and the scope of the groups, it is not something in which it will happen
to come across probably, but it is good to know it anyway.
First of all, it must be said that creating components while iterating a group
is not a problem at all and is done freely as it happens with the views. The
same applies to the destruction of components and entities, for which the rules
mentioned above apply.
The additional limitation arises instead when a given component that is owned by
a group is iterated outside of it. In this case, adding components that are part
of the group itself may invalidate the iterators. There are no further
limitations to the destruction of components and entities.
Fortunately, this is not always true. In fact, it almost never is and only
happens under certain conditions. In particular:
Iterating a type of component that is part of a group with a single type view
and adding to an entity all the components required to get it into the group
may invalidate the iterators.Iterating a type of component that is part of a group with a multi type view
and adding to an entity all the components required to get it into the group
can invalidate the iterators, unless users specify another type of component
to use to induce the order of iteration of the view (in this case, the former
is treated as a free type and is not affected by the limitation).
In other words, the limitation does not exist as long as a type is treated as a
free type (as an example with multi type views and partial- or non-owning
groups) or iterated with its own group, but it can occur if the type is used as
a main type to rule on an iteration.
This happens because groups own the pools of their components and organize the
data internally to maximize performance. Because of that, full consistency for
owned components is guaranteed only when they are iterated as part of their
groups or as free types with multi type views and groups in general.
Multithreading
In general, the entire registry is not thread safe as it is. Thread safety is
not something that users should want out of the box for several reasons. Just to
mention one of them: performance.
Views, groups and consequently the approach adopted by EnTT are the great
exception to the rule. It is true that views, groups and iterators in general
are not thread safe by themselves. Because of this users should not try to
iterate a set of components and modify the same set concurrently. However:
As long as a thread iterates the entities that have the component
Xor
assign and removes that component from a set of entities, another thread can
safely do the same with componentsYandZand everything work like just
fine. As a trivial example, users can freely execute the rendering system and
iterate the renderable entities while updating a physic component concurrently
on a separate thread.Similarly, a single set of components can be iterated by multiple threads as
long as the components are neither assigned nor removed in the meantime. In
other words, a hypothetical movement system can start multiple threads, each
of which will access the components that carry information about velocity and
position for its entities.
This kind of entity-component systems can be used in single-threaded
applications as well as along with async stuff or multiple threads. Moreover,
typical thread based models for ECS do not require a fully thread-safe
registry to work. Actually, users can reach the goal with the registry as it is
while working with most of the common models.
Because of the few reasons mentioned above and many others not mentioned, users
are completely responsible for synchronization whether required. On the other
hand, they could get away with it without having to resort to particular
expedients.
Finally, EnTT is configured via a few compile-time definitions to make some of
its parts implicitly thread-safe, roughly speaking only the ones that really
make sense and cannot be turned around.
When using multiple threads with EnTT, you should define ENTT_USE_ATOMIC
unless you know exactly what you are doing. This is true even if each thread
only uses thread local data. For more information, see
this section.
Iterators
A special mention is needed for the iterators returned by views and groups. Most
of the time they meet the requirements of random access iterators, in all cases
they meet at least the requirements of forward iterators.
In other terms, they are suitable for use with the parallel algorithms of the
standard library. If it is not clear, this is a great thing.
As an example, this kind of iterators are used in combination withstd::for_each and std::execution::par to parallelize the visit and therefore
the update of the components returned by a view or a group, as long as the
constraints previously discussed are respected:
auto view = registry.view<position, const velocity>();
std::for_each(std::execution::par_unseq, view.begin(), view.end(), [&view](auto entity) {
// ...
});This can increase the throughput considerably, even without resorting to who
knows what artifacts that are difficult to maintain over time.
Unfortunately, because of the limitations of the current revision of the
standard, the parallel std::for_each accepts only forward iterators. This
means that the default iterators provided by the library cannot return proxy
objects as references and must return actual reference types instead.
This may change in the future and the iterators will almost certainly return
both the entities and a list of references to their components by default sooner
or later. Multi-pass guarantee will not break in any case and the performance
should even benefit from it further.
Const registry
A const registry is also fully thread safe. This means that it is not able to
lazily initialize a missing storage when a view is generated.
The reason for this is easy to explain. To avoid requiring types to be
announced in advance, a registry lazily creates the storage objects for the
different components. However, this is not possible for a thread safe const
registry.
Returned views are always valid and behave as expected in the context of the
caller. However, they may contain dangling references to non-existing storage
when created from a const registry.
As a result, such a view may misbehave over time if it is kept aside for a
second use.
Therefore, if the general advice is to create views when necessary and discard
them immediately afterwards, this becomes almost a rule when it comes to views
generated from a const registry.
Fortunately, there is also a way to instantiate storage classes early when in
doubt or when there are special requirements.
Calling the storage method is equivalent to announcing a particular storage,
so as to avoid running into problems. For those interested, there are also
alternative approaches, such as a single threaded tick for the registry warm-up,
but these are not always applicable.
In this case, views never risk becoming invalid.
Beyond this document
There are many other features and functions not listed in this document.EnTT and in particular its ECS part is in continuous development and some
things could be forgotten, others could have been omitted on purpose to reduce
the size of this file. Unfortunately, some parts may even be outdated and still
to be updated.
For further information, it is recommended to refer to the documentation
included in the code itself or join the official channels to ask a question.
Md Faq (`docs/md/faq.md`)
Frequently Asked Questions
Table of Contents
Introduction
This is a constantly updated section where I am trying to put the answers to the
most frequently asked questions.
If you do not find your answer here, there are two cases: nobody has done it
yet, or this section needs updating. In both cases, you can
open a new issue or enter either
the gitter channel or the
discord server to ask for help.
Probably someone already has an answer for you and we can then integrate this
part of the documentation.
FAQ
Why is my debug build on Windows so slow?
EnTT is an experimental project that I also use to keep me up-to-date with the
latest revision of the language and the standard library. For this reason, it is
likely that some classes you are working with are using standard containers
under the hood.
Unfortunately, it is known that the standard containers are not particularly
performing in debugging (the reasons for this go beyond this document) and are
even less so on Windows, apparently. Fortunately, this can also be mitigated a
lot, achieving good results in many cases.
First of all, there are two things to do in a Windows project:
Disable the
/JMC
option (Just My Code debugging), available starting with Visual Studio 2017
version 15.8.Set the
_ITERATOR_DEBUG_LEVEL
macro to 0. This will disable checked iterators and iterator debugging.
Moreover, set the ENTT_DISABLE_ASSERT variable or redefine the ENTT_ASSERT
macro to disable internal debug checks in EnTT:
#define ENTT_ASSERT(...) ((void)0)These asserts are introduced to help the users, but they require access to the
underlying containers and therefore risk ruining the performance in some cases.
With these changes, debug performance should increase enough in most cases. If
you want something more, you can also switch to an optimization level O0 or
preferably O1.
How can I represent hierarchies with my components?
This is one of the first questions that anyone makes when starting to work with
the entity-component-system architectural pattern.
There are several approaches to the problem, and the best one depends mainly on
the real problem one is facing. In all cases, how to do it does not strictly
depend on the library in use, but the latter certainly allows or not different
techniques depending on how the data are laid out.
I tried to describe some of the approaches that fit well with the model ofEnTT. This is the
first post of a series that tries to explore the problem. More will probably
come in the future.
In addition, EnTT also offers the possibility to create stable storage types
and therefore have pointer stability for one, all or some components. This is by
far the most convenient solution when it comes to creating hierarchies and
whatnot. See the documentation for the ECS part of the library and in particular
what concerns the component_traits class for further details.
Custom entity identifiers: yay or nay?
Custom entity identifiers are definitely a good idea in two cases at least:
If
std::uint32_tis not large enough for your purposes, since this is the
underlying type ofentt::entity.If you want to avoid conflicts when using multiple registries.
Identifiers can be defined through enum classes and class types that define anentity_type member of type std::uint32_t or std::uint64_t.
In fact, this is a definition equivalent to that of entt::entity:
enum class entity: std::uint32_t {};There is no limit to the number of identifiers that can be defined.
Warning C4003: the min, the max and the macro
On Windows, a header file defines two macros min and max which may result in
conflicts with their counterparts in the standard library and therefore in
errors during compilation.
It is a pretty big problem. However, fortunately it is not a problem of EnTT
and there is a fairly simple solution to it.
It consists in defining the NOMINMAX macro before including any other header
so as to get rid of the extra definitions:
#define NOMINMAXPlease refer to this issue for
more details.
The standard and the non-copyable types
EnTT uses internally the trait std::is_copy_constructible_v to check if a
component is actually copyable. However, this trait does not really check
whether a type is actually copyable. Instead, it just checks that a suitable
copy constructor and copy operator exist.
This can lead to surprising results due to some idiosyncrasies of the standard.
For example, std::vector defines a copy constructor that is conditionally
enabled depending on whether the value type is copyable or not. As a result,std::is_copy_constructible_v returns true for the following specialization:
struct type {
std::vector<std::unique_ptr<action>> vec;
};However, the copy constructor is effectively disabled upon specialization.
Therefore, trying to assign an instance of this type to an entity may trigger a
compilation error.
As a workaround, users can mark the type explicitly as non-copyable. This also
suppresses the implicit generation of the move constructor and operator, which
will therefore have to be defaulted accordingly:
struct type {
type(const type &) = delete;
type(type &&) = default;
type & operator=(const type &) = delete;
type & operator=(type &&) = default;
std::vector<std::unique_ptr<action>> vec;
};Note that aggregate initialization is also disabled as a consequence.
Fortunately, this type of trick is quite rare. The bad news is that there is no
way to deal with it at the library level, this being due to the design of the
language. On the other hand, the fact that the language itself also offers a way
to mitigate the problem makes it manageable.
Which functions trigger which signals
Storage classes offer three signals that are emitted following specific
operations. Maybe not everyone knows what these operations are, though.
If this is not clear, below you can find a vademecum for this purpose:
on_createdis invoked when a component is first added (neither modified nor
replaced) to an entity.on_updateis called whenever an existing component is modified or replaced.on_destroyedis called when a component is explicitly or implicitly removed
from an entity.
Among the most controversial functions can be found emplace_or_replace anddestroy. However, following the above rules, it is quite simple to know what
will happen.
In the first case, on_created is invoked if the entity has not the component,
otherwise the latter is replaced and therefore on_update is triggered. As for
the second case, components are removed from their entities and thus freed when
they are recycled. It means that on_destroyed is triggered for every component
owned by the entity that is destroyed.
Duplicate storage for the same component
It is rare, but you can see double sometimes, especially when it comes to
storage. This can be caused by a conflict in the hash assigned to the various
component types (one of a kind) or by bugs in your compiler
(more common apparently).
Regardless of the cause, EnTT offers a customization point that also serves as
a solution in this case:
template<>
struct entt::type_hash<Type> final {
[[nodiscard]] static consteval id_type value() noexcept {
return hashed_string::value("Type");
}
[[nodiscard]] consteval operator id_type() const noexcept {
return value();
}
};Specializing type_hash directly bypasses the default implementation offered byEnTT, thus avoiding any possible conflicts or compiler bugs.
Md Graph (`docs/md/graph.md`)
Crash Course: graph
Table of Contents
Introduction
EnTT does not aim to offer everything one needs to work with graphs.
Therefore, anyone looking for this in the graph submodule will be
disappointed.
Quite the opposite is true though. This submodule is minimal and contains only
the data structures and algorithms strictly necessary for the development of
some tools such as the flow builder.
Data structures
As anticipated in the introduction, the aim is not to offer all possible data
structures suitable for representing and working with graphs. Many will likely
be added or refined over time. However, I want to discourage anyone expecting
tight scheduling on the subject.
The data structures presented in this section are mainly useful for the
development and support of some tools that are also part of the same submodule.
Adjacency matrix
The adjacency matrix is designed to represent either a directed or an undirected
graph:
entt::adjacency_matrix<entt::directed_tag> adjacency_matrix{};The directed_tag type creates the graph as directed. There is also anundirected_tag counterpart which creates it as undirected.
The interface deviates slightly from the typical double indexing of C and offers
an API that is perhaps more familiar to a C++ programmer. Therefore, the access
and modification of an element takes place via the contains, insert anderase functions rather than a double call to an operator[]:
if(adjacency_matrix.contains(0u, 1u)) {
adjacency_matrix.erase(0u, 1u);
} else {
adjacency_matrix.insert(0u, 1u);
}Both insert and erase are idempotent functions which have no effect if the
element already exists or has already been deleted.
The first one returns an std::pair containing the iterator to the element and
a boolean value indicating whether the element was newly inserted or not. The
second one returns the number of deleted elements (0 or 1).
An adjacency matrix is initialized with the number of elements (vertices) when
constructing it but can also be resized later using the resize function:
entt::adjacency_matrix<entt::directed_tag> adjacency_matrix{3u};To visit all vertices, the class offers a function named vertices that returns
an iterable object suitable for the purpose:
for(auto &&vertex: adjacency_matrix.vertices()) {
// ...
}The same result is obtained with the following snippet, since the vertices are
plain unsigned integral values:
for(auto last = adjacency_matrix.size(), pos = {}; pos < last; ++pos) {
// ...
}As for visiting the edges, a few functions are available.
When the purpose is to visit all the edges of a given adjacency matrix, theedges function returns an iterable object that is used to get them as pairs of
vertices:
for(auto [lhs, rhs]: adjacency_matrix.edges()) {
// ...
}If the goal is to visit all the in- or out-edges of a given vertex instead, thein_edges and out_edges functions are meant for that:
for(auto [lhs, rhs]: adjacency_matrix.out_edges(3u)) {
// ...
}Both the functions expect the vertex to visit (that is, to return the in- or
out-edges for) as an argument.
Finally, the adjacency matrix is an allocator-aware container and offers most of
the functionalities one would expect from this type of containers, such asclear or get_allocator and so on.
Graphviz dot language
As it is one of the most popular formats, the library offers minimal support for
converting a graph to a Graphviz dot snippet.
The simplest way is to pass both an output stream and a graph to the dot
function:
std::ostringstream output{};
entt::dot(output, adjacency_matrix);It is also possible to provide a callback to which the vertices are passed and
which can be used to add (dot) properties to the output as needed:
std::ostringstream output{};
entt::dot(output, adjacency_matrix, [](auto &output, auto vertex) {
out << "label=\"v\"" << vertex << ",shape=\"box\"";
});This second mode is particularly convenient when the user wants to associate
externally managed data to the graph being converted.
Flow builder
A flow builder is used to create execution graphs from tasks and resources.
The implementation is as generic as possible and does not bind to any other part
of the library.
This class is designed as a sort of state machine to which a specific task is
attached for which the resources accessed in read-only or read-write mode are
specified.
Most of the functions in the API also return the flow builder itself, according
to what is the common sense API when it comes to builder classes.
Once all tasks are registered and resources assigned to them, an execution graph
in the form of an adjacency matrix is returned to the user.
This graph contains all the tasks assigned to the flow builder in the form of
vertices. The vertex itself is used as an index to get the identifier passed
during registration.
Tasks and resources
Although these terms are used extensively in the documentation, the flow builder
has no real concept of tasks and resources.
This class works mainly with identifiers, that is, values of type id_type.
In other terms, both tasks and resources are identified by integral values.
This allows not to couple the class itself to the rest of the library or to any
particular data structure. On the other hand, it requires the user to keep track
of the association between identifiers and operations or actual data.
Once a flow builder is created (which requires no constructor arguments), the
first thing to do is to bind a task. This tells the builder who intends to
consume the resources that are specified immediately after:
entt::flow builder{};
builder.bind("task_1"_hs);The example uses the EnTT hashed string to generate an identifier for the
task.
Indeed, the use of id_type as an identifier type is not by accident. In fact,
it matches well with the internal hashed string class. Moreover, it is also the
same type returned by the hash function of the internal RTTI system, in case the
user wants to rely on that.
However, being an integral value, it leaves the user full freedom to rely on his
own tools if necessary.
Once a task is associated with the flow builder, it has also assigned read-only
or read-write resources as appropriate:
builder
.bind("task_1"_hs)
.ro("resource_1"_hs)
.ro("resource_2"_hs)
.bind("task_2"_hs)
.rw("resource_2"_hs)As mentioned, many functions return the builder itself, and it is therefore easy
to concatenate the different calls.
Also in the case of resources, they are identified by numeric values of typeid_type. As above, the choice is not entirely random. This goes well with the
tools offered by the library while leaving room for maximum flexibility.
Finally, both the ro and rw functions also offer an overload that accepts a
pair of iterators, so that one can pass a range of resources in one go.
Rebinding
The flow class is resource based rather than task based. This means that graph
generation is driven by resources and not by the order of appearance of tasks
during flow definition.
Although this concept is particularly important, it is almost irrelevant for the
vast majority of cases. However, it becomes relevant when rebinding resources
or tasks.
In fact, nothing prevents rebinding elements to a flow.
However, the behavior changes slightly from case to case and has some nuances
that it is worth knowing about.
Directly rebinding a resource without the task being replaced trivially results
in the task's access mode for that resource being updated:
builder.bind("task"_hs).rw("resource"_hs).ro("resource"_hs)In this case, the resource is accessed in read-only mode, regardless of the
first call to rw.
Behind the scenes, the call does not actually replace the previous one but is
queued after it instead, overwriting it when generating the graph. Thus, a large
number of resource rebindings may even impact processing times (very difficult
to observe but theoretically possible).
Rebinding resources and also combining it with changes to tasks has far more
implications instead.
As mentioned, graph generation takes place starting from resources and not from
tasks. Therefore, the result may not be as expected:
builder
.bind("task_1"_hs)
.ro("resource"_hs)
.bind("task_2"_hs)
.ro("resource"_hs)
.bind("task_1"_hs)
.rw("resource"_hs);What happens here is that the resource first sees a read-only access request
from the first task, then a read-only request from the second task and finally
a read-write request from the first task.
Although this definition would probably be counted as an error, the resulting
graph may be unexpected. In fact, this consists of a single edge outgoing from
the second task and directed to the first task.
To intuitively understand what happens, it is enough to think of the fact that a
task never has an edge pointing to itself.
While not obvious, this approach has its pros and cons like any other solution.
For example, creating loops is actually simple in the context of resource-based
graph generations:
builder
.bind("task_1"_hs)
.rw("resource"_hs)
.bind("task_2"_hs)
.rw("resource"_hs)
.bind("task_1"_hs)
.rw("resource"_hs);As expected, this definition leads to the creation of two edges that define a
loop between the two tasks.
As a general rule, rebinding resources and tasks is highly discouraged because
it could lead to subtle bugs if users do not know what they are doing.
However, once the mechanisms of resource-based graph generation are understood,
it can offer to the expert user flexibility and a range of possibilities
otherwise inaccessible.
Fake resources and order of execution
The flow builder does not offer the ability to specify when a task should run
before or after another task.
In fact, the order of registration on the resources also determines the order
in which the tasks are processed during the generation of the execution graph.
However, there is a way to force the execution order of two processes.
Briefly, since accessing a resource in opposite modes requires sequential rather
than parallel scheduling, it is possible to make use of fake resources to rule
on the execution order:
builder
.bind("task_1"_hs)
.ro("resource_1"_hs)
.rw("fake"_hs)
.bind("task_2"_hs)
.ro("resource_2"_hs)
.ro("fake"_hs)
.bind("task_3"_hs)
.ro("resource_2"_hs)
.ro("fake"_hs)This snippet forces the execution of task_1 before task_2 and task_3.
This is due to the fact that the former sets a read-write requirement on a fake
resource that the other tasks also want to access in read-only mode.
Similarly, it is possible to force a task to run after a certain group:
builder
.bind("task_1"_hs)
.ro("resource_1"_hs)
.ro("fake"_hs)
.bind("task_2"_hs)
.ro("resource_1"_hs)
.ro("fake"_hs)
.bind("task_3"_hs)
.ro("resource_2"_hs)
.rw("fake"_hs)In this case, since there are a number of processes that want to read a specific
resource, they will do so in parallel by forcing task_3 to run after all the
other tasks.
Sync points
Sometimes it is useful to assign the role of sync point to a node.
Whether it accesses new resources or is simply a watershed, the procedure for
assigning this role to a vertex is always the same. First it is tied to the flow
builder, then the sync function is invoked:
builder.bind("sync_point"_hs).sync();The choice to assign an identity to this type of node lies in the fact that,
more often than not, they also perform operations on resources.
If this is not the case, it will still be possible to create no-op vertices to
which empty tasks are assigned.
Execution graph
Once both the resources and their consumers have been properly registered, the
purpose of this tool is to generate an execution graph that takes into account
all specified constraints to return the best scheduling for the vertices:
entt::adjacency_matrix<entt::directed_tag> graph = builder.graph();Searching for the main vertices (that is, those without in-edges) is usually the
first thing required:
for(auto &&vertex: graph) {
if(auto in_edges = graph.in_edges(vertex); in_edges.begin() == in_edges.end()) {
// ...
}
}Then it is possible to instantiate an execution graph by means of other
functions such as out_edges to retrieve the children of a given task oredges to get the identifiers.
Md Lib (`docs/md/lib.md`)
Push EnTT across boundaries
Table of Contents
Working across boundaries
EnTT has historically had a limit when used across boundaries on Windows in
general and on GNU/Linux when default visibility was set to hidden. The
limitation was mainly due to a custom utility used to assign unique, sequential
identifiers with different types.
Fortunately, nowadays EnTT works smoothly across boundaries.
Smooth until proven otherwise
Many classes in EnTT make extensive use of type erasure for their purposes.
This raises the need to identify objects whose type has been erased.
The type_hash class template is how identifiers are generated and thus made
available to the rest of the library. In general, this class arouses little
interest. The only exception is when a conflict between identifiers occurs
(definitely uncommon though) or when the default solution proposed by EnTT is
not suitable for the user's purposes.
The section dedicated to type_info contains all the details to get around the
issue in a concise and elegant way. Please refer to the specific documentation.
When working with linked libraries, compile definitions ENTT_API_EXPORT andENTT_API_IMPORT are there to import or export symbols, so as to make
everything work nicely across boundaries.
On the other hand, everything should run smoothly when working with plugins or
shared libraries that do not export any symbols.
For those who need more details, the test suite contains many examples covering
the most common cases (see the lib directory for all details).
It goes without saying that it is impossible to cover all possible cases.
However, what is offered should hopefully serve as a basis for all of them.
Meta context
The runtime reflection system deserves a special mention when it comes to using
it across boundaries.
Since it is linked already to a static context to which the elements are
attached and different contexts do not relate to each other, they must be
shared to allow the use of meta types across boundaries.
Fortunately, sharing a context is also trivial to do. First of all, the local
one is acquired in the main space:
auto handle = entt::locator<entt::meta_ctx>::handle();Then, it is passed to the receiving space that sets it as its default context,
thus discarding or storing aside the local one:
entt::locator<entt::meta_ctx>::reset(handle);From now on, both spaces refer to the same context and to it are all new meta
types attached, no matter where they are created.
Note that replacing the main context does not also propagate changes across
boundaries. In other words, replacing a context results in the decoupling of the
two sides and therefore a divergence in the contents.
Memory Management
There is another subtle problem due to memory management that can lead to
headaches.
It can occur where there are pools of objects (such as components or events)
dynamically created on demand. This is usually not a problem when working with
linked libraries that rely on the same dynamic runtime. However, it can occur in
the case of plugins or statically linked runtimes.
As an example, imagine creating an instance of registry in the main executable
and sharing it with a plugin. If the latter starts working with a component that
is unknown to the former, a dedicated pool is created within the registry on
first use.
As one can guess, this pool is instantiated on a different side of the boundary
from the registry. Therefore, the instance is now managing memory from
different spaces, and this can quickly lead to crashes if not properly
addressed.
To overcome the risk, it is recommended to use well-defined interfaces that make
fundamental types pass through the boundaries, isolating the instances of theEnTT classes from time to time and as appropriate.
Refer to the test suite for some examples, read the documentation available
online about this type of issues or consult someone who has already had such
experiences to avoid problems.
Md Links (`docs/md/links.md`)
EnTT in Action
Table of Contents
Introduction
EnTT is widely used in private and commercial applications. I cannot even
mention most of them because of some signatures I put on some documents time
ago. Fortunately, there are also people who took the time to implement open
source projects based on EnTT and did not hold back when it came to
documenting them.
Below an incomplete list of games, applications and articles that can be used as
a reference.
Where I put the word apparently means that the use of EnTT is documented but
the authors did not make explicit announcements or contacted me directly.
If you know of other resources out there that are about EnTT, feel free to
open an issue or a PR. I will be glad to add them to this page.
I hope the following lists can grow much more in the future.
EnTT in Action
Games
- Minecraft by
Mojang: of course, that Minecraft, see the
open source attributions page for more details. - Minecraft Legends by
Mojang: an action strategy game where users have to
fight to defend the Overworld. - Minecraft Earth by
Mojang: an augmented reality game for mobile, that
lets users bring Minecraft into the real world. - Ember Sword: a modern Free-to-Play MMORPG with a
player-driven economy, a classless combat system, and scarce, tradable
cosmetic collectibles. - Apparently Diablo II: Resurrected by
Blizzard: monsters, heroes, items, spells, all
resurrected. Thanks unknown insider. - Apparently
Call of Duty: Vanguard by
Sledgehammer Games: I can neither
confirm nor deny, but there is a license I know in the credits. - Apparently D&D Dark Alliance by
Wizards of the Coast: your party, their
funeral. - TiltedEvolution by
Tilted Phoques: Skyrim and Fallout 4 mod
to play online. - Antkeeper: an ant colony
simulation game. - Openblack: open source
reimplementation of the game Black & White (2001). - Land of the Rair: the new backend
of a retro-style MUD for the new age. - Face Smash:
a game to play with your face. - EnTT Pacman: an example of how
to make Pacman withEnTT. - Wacman: a pacman clone with OpenGL.
- Classic Tower Defence:
a tiny little tower defence game featuring a homemade font.
Check it out. - The Machine: a box pushing
puzzler with logic gates and other cool stuff.
Check it out. - EnTTPong: a basic game made to showcase
different parts ofEnTTand C++17. - Randballs: simple
SFMLandEnTT
playground. - EnTT Tower Defense: a data oriented tower
defense example. - EnTT Breakout: simple
example of a breakout game, usingSDLandEnTT. - Arcade puzzle game with EnTT:
arcade puzzle game made in C++ using theSDL2andEnTTlibraries. - Snake with EnTT: simple snake game
made in C++ with theSDL2andEnTTlibraries. - Mirrors lasers and robots:
a small tower defense game based on mirror orientation. - PopHead: 2D, Zombie,
RPG game made from scratch in C++. - Robotligan: multiplayer
football game. - DungeonSlayer: 2D game made
from scratch in C++. - 3DGame: 2.5D top-down space shooter.
- Pulcher: 2D cross-platform game inspired
by Quake. - Destroid: one-bazillionth
arcade game about shooting dirty rocks in space, inspired by Asteroids. - Wanderer: a 2D
exploration-based indie game. - Spelunky® Classic remake: a truly
multiplatform experience with a rewrite from scratch. - CubbyTower: a simple tower
defense game using C++ with Entity Component System (ECS). - Runeterra: Legends of Runeterra
simulator using C++ with some reinforcement learning. - Black Sun: fly your
spaceship through a large 2D open world. - PokeMaster: Pokémon Battle
simulator using C++ with some reinforcement learning. - HomeHearth: choose your hero, protect the
town, before it is too late. - City Builder Game: a simple
city-building game using C++ and OpenGL. - BattleSub: two player 2D submarine
game with some fluid dynamics. - Crimson Rush: a dungeon-crawler and
rougelike inspired game about exploring and surviving as long as possible. - Space Fight: one screen
multi-player arcade shooter game prototype. - Confetti Party: C++ sample
application as a starting point usingEnTTandSDL2. - Hellbound: a top-down action rogue-like
where to fight colossal demons in procedurally generated levels of hell. - Saurian Sorcery: a tower defense
game where to assemble a tribe of lizards to defend against robot invaders. - robotfindskitten: a clone
ofrobotfindskitteninsideNotepad.exe, powered byEnTT. - Orion: Outer-space Research and
Interstellar Observation Network (a space shooter game). - EnTT Boids: a simple boids
implementation usingEnTTandRaylib. - PalmRide: After Flight:
an on-rails shooter with retro outrun aesthetics. - Exhibition of Speed:
build your own car and go racing. - Lichgate: top-down action rogue-like where
users unlock abilities to fight horde of enemies in an endless world. - Letalka:
small demo game with ships and bullets flying everywhere on the screen. - Lichgate: step into the robes of a powerful
mage determined to halt the relentless hordes of undead. - You Are Circle:
a roguelite top-down shooter with a high-contrast vector line aesthetic. - EnTT Dino: a Dinosaur Game
clone in C++ using onlySDL2andEnTT. - Bim!: a last-man-standing arcade online
game for Android. - MonsterWar: a tower defense game
developed in C++ withSDL3,EnTT, and a few other libraries.
Engines and the like:
- Hazel Engine: a work in progress
engine created by The Cherno during
one of his most famous video series. - Aether Engine
v1.1+ by
Hadean: a library designed for spatially partitioning
agent-based simulations. - Fling Engine: a Vulkan game
engine with a focus on data oriented design. - NovusCore: a modern take on World
of Warcraft emulation. - Chrysalis: action RPG SDK for
CRYENGINE games. - LM-Engine: the Vim of game
engines. - Edyn: a real-time physics engine
organized as an ECS. - MushMachine: engine...
vrooooommm. - Antara Gaming SDK:
the Komodo Gaming Software Development Kit. - XVP:
eXpansive Vehicle Physics
plugin for Unreal Engine. - Apparently
Wisp by
Team Wisp: an advanced real-time ray tracing
renderer built for the demands of video game artists. - shiva: modern C++ engine with
modularity. - ImGui/EnTT editor:
a drop-in, single-file entity editor forEnTTthat usesImGuias
graphical backend (with
demo code). - SgOgl: a game engine library for OpenGL
developed for educational purposes. - Lumos: game engine written in C++
using OpenGL and Vulkan. - Silvanus: Silvanus Fusion 360
Box Generator. - Lina Engine: an open-source,
modular, tiny and fast C++ game engine, aimed to develop 3D desktop games. - Spike: a powerful game engine which
can run on a toaster. - Helena Framework: a modern
framework in C++17 for backend development. - Unity/EnTT: tech demo of a
native simulation layer usingEnTTandUnityas a rendering engine. - OverEngine: an overengineered
game engine. - Electro: high performance
3D game engine with a high emphasis on rendering. - Kawaii: a modern data
oriented game engine. - Becketron: a game engine
written mostly in C++. - Spatial Engine: a
cross-platform engine created on top of Google's filament rendering engine. - Kaguya: D3D12 Rendering Engine.
- OpenAWE: open implementation
of the Alan Wake Engine. - Nazara Engine: fast,
cross-platform, object-oriented API to help in daily developer life. - Billy Engine: some kind of 2D
engine based onSDL2andEnTT. - Ducktape: an open source C++
2D & 3D game engine that focuses on being fast and powerful. - The Worst Engine: a game engine based on
OpenGL. - Ecsact: a language aimed at describing ECS, with a
runtime implementation based
onEnTT. - AGE (Arc Game Engine): an
open-source engine for building 2D & 3D real-time rendering and interactive
contents. - Kengine: the Koala engine is a game
engine entirely implemented as an entity-component-system. - Scion2D: 2D game engine with
YouTube series
included. - EnTT Editor: an editor for
EnTT
libary that combines its built-in reflection system withImGui. - Era Game Engine: a modern
ECS-based game engine. - Core SDK of Trollworks engine: 2D
game engine based on procrastination. - Rocky: 3D geospatial application
engine. - Donner: a modern C++20 SVG2 rendering
API with CSS3. - Coral Engine: open-source
student engine with the tools to make games in C++ and Visual scripting. - Star Engine: an Advanced C++
DirectX 11 game engine. - Darmok: another C++ game engine.
- Magique: 2D game engine for programmers
(or those yet to be). - Physecs: real-time 3D rigid body
physics simulation built onEnTT. - KODZA: A work in progress game engine.
- Omnix: An under-development,
multi-purpose 3D engine formacOS, usingEnTTfor the ECS.
Articles, videos and blog posts:
- Some posts on my personal
blog are aboutEnTT, for those who want to
know more on this project. - Game Engine series by
The Cherno (not only aboutEnTTbut also
on the use of an ECS in general):- Intro to EnTT.
- Entities and Components.
- The ENTITY Class.
- Camera Systems.
- Scene Camera.
- Native Scripting.
- Native Scripting (now with virtual functions!).
- Scene Hierarchy Panel.
- Properties Panel.
- Camera Component UI.
- Drawing Component UI.
- Transform Component UI.
- Adding/Removing Entities and Components UI.
- Saving and Loading Scenes.
- ... And so on.
Check out the
Game Engine Series by The Cherno for more videos.
- Game Engine series by
dwjclark11 (not justEnTTbut a lot of
it):- Getting into ECS
- Creating ECS Wrapper Classes
- Runtime Reflection using EnTT meta
- Adding entt::meta and Sol2 bindings
(with part two too) - ... And so on.
Check it out
for more videos.
- Warmonger Dynasty devlog series
by linkdd: an interesting walkthrough of
developing a game (also) with EnTT. - Use EnTT When You Need An ECS
by Thomas: I could not have said it
better. - Space Battle: Huge edition:
huge space battle built entirely from scratch. - Space Battle: huge space
battle built onUE4. - Experimenting with ECS in UE4:
interesting article aboutUE4andEnTT. - Implementing ECS architecture in UE4:
giant space battle. - Conan Adventures (SFML and EnTT in C++):
create projects in modern C++ usingSFML,EnTT,ConanandCMake. - Adding EnTT ECS to Chrysalis:
a blog entry (and its
follow-up)
about the integration ofEnTTintoChrysalis, an action RPG SDK for
CRYENGINE games. - Creating Minecraft in One Week with C++ and Vulkan:
a crack at recreating Minecraft in one week using a custom C++ engine and
Vulkan (code included). - Ability Creator:
project retrospect by Eric Hildebrand. - EnTT Entity Component System Gaming Library:
EnTTon GameFromScratch.com. - Custom C++ server for UE5 optimized for
MMO(RPG)s and its follow-up episode about
player bots and full external ECS: a series definitely worth looking at.
Any Other Business:
- ArcGIS Runtime SDKs by
Esri: they useEnTTfor the internal ECS and the
cross-platform C++ rendering engine. The SDKs are used by a lot of
enterprise custom apps, as well as by Esri for its own public applications
such as
Explorer,
Collector
and
Navigator. - OneArc: licenses
do not lie. Their products use EnTT in some way, but it is not known what
way. - FASTSUITE Edition 2
by Cenit: they useEnTTto drive their simulation, that is, the communication between robot
controller emulator and renderer. - Ragdoll: real-time physics for Autodesk Maya
- Project Lagrange: a robust geometry
processing library by Adobe. - AtomicDEX: a secure
wallet and noncustodial decentralized exchange rolled into one application. - Apparently
NIO: there was a collaboration to make some changes
toEnTT, at the time used for internal projects. - Apparently
Tieto: they published a job post whereEnTTwas
listed on their software stack. - Sequentity: A MIDI-like
sequencer/tracker for C++ andImGui(withMagnumandEnTT). - EnTT meets Sol2: freely
available examples of how to combineEnTTandSol2. - Godot meets EnTT:
a simple example on how to useEnTTwithinGodot. - Godot and GameNetworkingSockets meet EnTT:
a simple example on how to useEnTTandGameNetworkingSockets
withinGodot. - MatchOneEntt: port of
Match One forEntitas-CSharp. - GitHub contains also
many other examples
of use ofEnTTfrom which to take inspiration if interested.
Md Locator (`docs/md/locator.md`)
Crash Course: service locator
Table of Contents
Introduction
Usually, service locators are tightly bound to the services they expose. It is
hard to define a general purpose solution.
This tiny class tries to fill the gap and gets rid of the burden of defining a
different specific locator for each application.
Service locator
The service locator API tries to mimic that of std::optional and adds some
extra functionalities on top of it such as allocator support.
There are a couple of functions to set up a service, namely emplace andallocate_emplace:
entt::locator<interface>::emplace<service>(argument);
entt::locator<interface>::allocate_emplace<service>(allocator, argument);The difference is that the latter expects an allocator as the first argument and
uses it to allocate the service itself.
Once a service is set up, it is retrieved using the value function:
interface &service = entt::locator<interface>::value();Since the service may not be set (and therefore this function may result in an
undefined behavior), the has_value and value_or functions are also available
to test a service locator and to get a fallback service in case there is none:
if(entt::locator<interface>::has_value()) {
// ...
}
interface &service = entt::locator<interface>::value_or<fallback_impl>(argument);All arguments are used only if necessary, that is, if a service does not already
exist and therefore the fallback service is constructed and returned. In all
other cases, they are discarded.
Finally, to reset a service, use the reset function.
Opaque handles
Sometimes it is useful to transfer a copy of a service to another locator. For
example, when working across boundaries it is common to share a service with a
dynamically loaded module.
Options are not much in this case. Among these is the possibility of exporting
services and assigning them to a different locator.
This is what the handle and reset functions are meant for.
The former returns an opaque object useful for exporting (or rather, obtaining
a reference to) a service. The latter also accepts an optional argument to a
handle which then allows users to reset a service by initializing it with an
opaque handle:
auto handle = entt::locator<interface>::handle();
entt::locator<interface>::reset(handle);It is worth noting that it is possible to get handles for uninitialized services
and use them with other locators. Of course, all a user will get is to have an
uninitialized service elsewhere as well.
Note that exporting a service allows users to share the object currently set
in a locator. Replacing it will not replace the element, even where a service
has been configured with a handle to the previous item.
In other words, if an audio service is replaced with a null object to silence an
application and the original service was shared, this operation will not
propagate to the other locators. Therefore, a module that shares the ownership
of the original audio service is still able to emit sounds.
Md Meta (`docs/md/meta.md`)
Crash Course: runtime reflection system
Table of Contents
- Introduction
- Identifiers
- Reflection in a nutshell
Introduction
Reflection (or rather, its lack) is a trending topic in the C++ world and a tool
that can unlock a lot of interesting features in the specific case of EnTT. I
looked for a third-party library that met my needs on the subject, but I always
came across some details that I did not like: macros, being intrusive, too many
allocations, and so on.
I finally decided to write a built-in, non-intrusive and macro-free runtime
reflection system for EnTT. Maybe I did not do better than others or maybe
yes, time will tell me, but at least I can model this tool around the library to
which it belongs and not the opposite.
Identifiers
The meta system does not force users to rely on the tools provided by the
library when it comes to working with identifiers. It does this by offering an
API that works with integral values that may or may not be generated by means of
a hashed string.
This means that users can assign any type of identifier to the meta objects, as
long as they are numeric. It does not matter if they are generated at runtime,
at compile-time or with custom functions.
That being said, some of the examples in the following sections are based on thehashed_string class as provided by this library. Therefore, where an integral
identifier is provided, it is likely that a user defined literal is used as
follows:
entt::meta_factory<my_type>{}.type("reflected_type"_hs);For what it is worth, this is completely equivalent to:
entt::meta_factory<my_type>{}.type(42u);Obviously, human-readable identifiers are more convenient to use and highly
recommended.
Reflection in a nutshell
Reflection always starts from actual C++ types. Users cannot reflect imaginary
types.
The meta_factory class is where it all starts:
entt::meta_factory<my_type> factory{};The returned value is a factory object to use to continue building the meta
type.
By default, a meta type is associated with the identifier returned by the
runtime type identification system built-in in EnTT.
However, it is also possible to assign custom identifiers upon registration to
meta types:
entt::meta_factory<my_type>{"reflected_type"_hs};By doing this, the type based lookup no longer finds the specified meta type.
Instead, only the identifier based lookup returns with success.
For the same reason, it is also possible to create multiple meta types for the
same C++ type.
In addition to identifiers, users can assign aliases to meta types. An alias
must be unique and not conflict with existing identifiers.
Meta type lookup attempts to resolve identifiers via aliases if there is no
matching key:
entt::meta_factory<my_type>{}.type("reflected_type"_hs);However, users can be interested in adding features to a reflected type so that
the reflection system can use it correctly under the hood, while they do not
want to also make the type searchable.
In this case, it is sufficient not to invoke type.
A factory is such that all its member functions return the factory itself. It is
generally used to create the following:
Constructors. A constructor is assigned to a reflected type by specifying
its list of arguments. Free functions are also accepted if the return type
is the expected one. From a client perspective, nothing changes between a free
function or an actual constructor:cppentt::meta_factory<my_type>{}.ctor<int, char>().ctor<&factory>();Meta default constructors are implicitly generated, if possible.
Data members. Meta data members are actual data members of the underlying
type but also static and global variables or constants of any kind. From the
point of view of the client, all the variables associated with the reflected
type appear as if they were part of the type itself:cppentt::meta_factory<my_type>{} .data<&my_type::static_variable>("static"_hs) .data<&my_type::data_member>("member"_hs) .data<&global_variable>("global"_hs);The
datafunction requires the identifier to use for the meta data member.
This is then used for runtime access.
Data members are also defined by means of a setter and getter pair. These are
either free functions, class members or a mix of them. This approach is also
convenient to create read-only properties from a non-const data member:cppentt::meta_factory<my_type>{}.data<nullptr, &my_type::data_member>("member"_hs);Setters and getters take any number of arguments, making the meta member more
like a function than a true member, while still retaining its nature.
This is especially convenient when constructing complex members or indexing
values into underlying structures of various kinds.Member functions. Meta member functions are actual member functions of the
underlying type but also plain free functions. From the point of view of the
client, all the functions associated with the reflected type appear as if they
were part of the type itself:cppentt::meta_factory<my_type>{} .func<&my_type::static_function>("static"_hs) .func<&my_type::member_function>("member"_hs) .func<&free_function>("free"_hs);The
funcfunction requires the identifier to use for the meta data function.
This is then used for runtime access.
Overloading of meta functions is supported. Overloaded functions are resolved
at runtime by the reflection system according to the types of the arguments.Base classes. A base class is such that the underlying type is actually
derived from it:cppentt::meta_factory<derived_type>{}.base<base_type>();The reflection system tracks the relationship and allows for implicit casts at
runtime when required. In other terms, wherever abase_typeis required, an
instance ofderived_typeis also accepted.Conversion functions. Conversion functions allow users to define conversions
that are implicitly performed by the reflection system when required:cppentt::meta_factory<double>{}.conv<int>();
This is everything users need to create meta types. Refer to the inline
documentation for further details.
Any to the rescue
The reflection system offers a kind of extended version of the entt::any
class (see the core module for more details).
The purpose is to add some feature on top of those already present, so as to
integrate it with the meta type system without having to duplicate the code.
The API is very similar to that of the any type. The class meta_any wraps
many of the feature to infer a meta node, before forwarding some or all of the
arguments to the underlying storage.
Among the few relevant differences, meta_any adds support for containers and
pointer-like types, while any does not.
Similar to any, this class is also used to create aliases for unmanaged
objects either with forward_as_meta or using the std::in_place_type<T &>
disambiguation tag, as well as from an existing object by means of the as_ref
member function. Additionally, it can take ownership of pointers passed as
arguments along with std::in_place.
Unlike any instead, meta_any treats an empty instance and one initialized
with void differently:
entt::meta_any empty{};
entt::meta_any other{std::in_place_type<void>};While any considers both as empty, meta_any treats objects initialized withvoid as if they were valid ones. This allows differentiating between failed
function calls and function calls that are successful but return nothing.
Finally, the member functions try_cast, cast and allow_cast are used to
cast the underlying object to a given type (either a reference or a value type)
or to convert a meta_any in such a way that a cast becomes viable for the
resulting object.
There is in fact no any_cast equivalent for meta_any.
Enjoy the runtime
Once the web of reflected types is constructed, it is a matter of using it at
runtime where required.
There are a few options to search for a reflected type:
// direct access to a reflected type
auto by_type = entt::resolve<my_type>();
// look up a reflected type by identifier
auto by_id = entt::resolve("reflected_type"_hs);
// look up a reflected type by type info
auto by_type_id = entt::resolve(entt::type_id<my_type>());There exists also an overload of the resolve function to use to iterate all
reflected types at once. It returns an iterable object to be used in a range-for
loop:
for(auto &&[id, type]: entt::resolve()) {
// ...
}In all cases, the returned value is an instance of meta_type (possibly with
its id). These objects offer an API to know their runtime identifiers, to
iterate all the meta objects associated with them and even to build instances of
the underlying type.
Meta data members and functions are accessed by means of their identifiers:
Meta data members:
cppauto data = entt::resolve<my_type>().data("member"_hs);The returned type is
meta_dataand may be invalid if there is no meta data
object associated with the given identifier.
A meta data object offers an API to query the underlying type (for example, to
know if it is a const or a static one), to get the meta type of the variable
and to set or get the contained value.Meta function members:
cppauto func = entt::resolve<my_type>().func("member"_hs);The returned type is
meta_funcand may be invalid if there is no meta
function object associated with the given identifier.
A meta function object offers an API to query the underlying type (for
example, to know if it is a const or a static function), to know the number of
arguments, the meta return type and the meta types of the parameters. In
addition, a meta function object is used to invoke the underlying function and
then get the return value in the form of ameta_anyobject.
Both functions search for the elements throughout the meta type hierarchy.
However, they offer the option of passing a second boolean argument to stop the
search at the top-level meta type.
All the meta objects thus obtained as well as the meta types explicitly convert
to a boolean value to check for validity:
if(auto func = entt::resolve<my_type>().func("member"_hs); func) {
// ...
}Furthermore, all of them (and a few more, like meta basis) are returned by a
bunch of overloads that provide the caller with iterable ranges of top-level
elements. As an example:
for(auto &&[id, type]: entt::resolve<my_type>().base()) {
// ...
}Meta types are also used to construct actual instances of the underlying
type.
In particular, the construct member function accepts a variable number of
arguments and searches for a match. It then returns a meta_any object that may
or may not be initialized, depending on whether a suitable constructor was found
or not.
Conversion functions are not accessible instead. They are used internally bymeta_any and the meta objects when needed.
Meta types and meta objects in general contain much more than what was said.
Refer to the inline documentation for further details.
Tell me your name
For meta types, data and functions, users can also provide custom names:
entt::meta_factory<my_type>{}
.type("type"_hs, "my_type")
.data<&variable>("data"_hs, "variable")
.func<&function>("func"_hs, "function");The label provided should be a string literal. The library does not make
copies. It is up to the user to guarantee the lifetime of the name itself.
String identifiers are returned from the meta objects via the name function:
const char *name = entt::resolve<my_type>().name();Since most of the time there is no need to differentiate the name from the
numeric identifier associated with a meta object, EnTT also offers a more
compact version of these functions:
entt::meta_factory<my_type>{}
.type("my_type")
.data<&variable>("variable")
.func<&function>("function");Again, the name provided should be a string literal. The string is then used
to generate a numeric identifier with the hashed_string class.
Despite support for names, there are no string-based lookup functions available.
That is, types (resolve) as well as data members (data) and function members
(func) are only searchable by numeric identifiers.
Container support
The runtime reflection system also supports containers of all types.
Moreover, containers does not necessarily mean those offered by the C++
standard library. In fact, user defined data structures can also work with the
meta system in many cases.
Containers are automatically detected based on some common traits.
For example, and not limited to, a sequence container must have a begin/end
pair that returns a forward iterator, while an associative container must also
provide a key_type member and a find function.
If a container is not recognized as such, it is still possible to provide an
adapter by specializing the template classes meta_sequence_container_traits
and meta_associative_container_traits. Similarly, users can inhibit the
detection of a type as a meta container by specializing the right traits class
but without providing any definition.
Standard library containers are generally exported as meta containers out of the
box (with the exception of std::string, which is not considered a sequence
container on purpose).
However, it is important to include the header file container.hpp to make
the right specializations available to the compiler when needed.
The same file also contains some examples for the users that are interested in
making their own containers available to the meta system.
For meta containers, the meta_any class returns properly initialized proxy
objects for ease of use. The following is a deliberately verbose example of how
users can access a proxy object for a sequence container:
std::vector<int> vec{1, 2, 3};
entt::meta_any any = entt::forward_as_meta(vec);
if(any.type().is_sequence_container()) {
if(auto view = any.as_sequence_container(); view) {
// ...
}
}Proxy object for associative containers are accessed in the same way by callingas_associative_container instead.
It is not necessary to perform a double check actually. Instead, it is enough to
query the meta type or verify that the proxy object is valid. In fact, proxies
are contextually convertible to bool to check for validity. For example, invalid
proxies are returned when the wrapped object is not a container.
In all cases, users are not expected to reflect containers explicitly. It is
sufficient to assign a container for which a specialization of the traits
classes exists to a meta_any object to be able to get its proxy object.
The interface of the meta_sequence_container proxy object is the same for all
types of sequence containers, although the available features differ from case
to case. In particular:
The
value_typemember function returns the meta type of the elements.The
sizemember function returns the number of elements in the container as
an unsigned integer value.The
resizemember function allows resizing the wrapped container and returns
true in case of success.
For example, it is not possible to resize fixed size containers.The
clearmember function allows clearing the wrapped container and returns
true in case of success.
For example, it is not possible to clear fixed size containers.The
reservemember function allows increasing the capacity of the wrapped
container and returns true in case of success.
For example, it is not possible to increase capacity of fixed size containers.The
beginandendmember functions return opaque iterators that is used to
iterate the container directly:cppfor(entt::meta_any element: view) { // ... }In all cases, given an underlying container of type
C, the returned element
contains an object of typeC::value_typewhich therefore depends on the
actual container.
All meta iterators are input iterators and do not offer an indirection
operator on purpose.The
insertmember function is used to add elements to the container. It
accepts a meta iterator and the element to insert:cppauto last = view.end(); // appends an integer to the container view.insert(last, 42);This function returns a meta iterator pointing to the inserted element and a
boolean value to indicate whether the operation was successful or not. A call
toinsertmay silently fail in case of fixed size containers or whether the
arguments are not at least convertible to the required types.
Since meta iterators are contextually convertible to bool, users can rely on
them to know if the operation failed on the actual container or upstream, for
example due to an argument conversion problem.The
erasemember function is used to remove elements from the container. It
accepts a meta iterator to the element to remove:cppauto first = view.begin(); // removes the first element from the container view.erase(first);This function returns a meta iterator following the last removed element and a
boolean value to indicate whether the operation was successful or not. A call
toerasemay silently fail in case of fixed size containers.The
operator[]is used to access container elements. It accepts a single
argument, the position of the element to return:cppfor(std::size_t pos{}, last = view.size(); pos < last; ++pos) { entt::meta_any value = view[pos]; // ... }The function returns instances of
meta_anythat directly refer to the actual
elements. Modifying the returned object directly modifies the element inside
the container.
Depending on the underlying sequence container, this operation may not be as
efficient. For example, in the case of anstd::list, a positional access
translates to a linear visit of the list itself (probably not what the user
expects).
Similarly, also the interface of the meta_associative_container proxy object
is the same for all types of associative containers. However, there are some
differences in behavior in the case of key-only containers. In particular:
The
key_onlymember function returns true if the wrapped container is a
key-only one.The
key_typemember function returns the meta type of the keys.The
mapped_typemember function returns an invalid meta type for key-only
containers and the meta type of the mapped values for all other types of
containers.The
value_typemember function returns the meta type of the elements.
For example, it returns the meta type ofintforstd::setwhile it
returns the meta type ofstd::pair<const int, char>forstd::map.The
sizemember function returns the number of elements in the container as
an unsigned integer value.The
clearmember function allows clearing the wrapped container and returns
true in case of success.The
reservemember function allows increasing the capacity of the wrapped
container and returns true in case of success.
For example, it is not possible to increase capacity of standard maps.The
beginandendmember functions return opaque iterators that are used
to iterate the container directly:cppfor(std::pair<entt::meta_any, entt::meta_any> element: view) { // ... }In all cases, given an underlying container of type
C, the returned element
is a key-value pair where the key has typeC::key_typeand the value has
typeC::mapped_type. Since key-only containers do not have a mapped type,
their value is nothing more than an invalidmeta_anyobject.
All meta iterators are input iterators and do not offer an indirection
operator on purpose.While the accessed key is usually constant in the associative containers and
is therefore returned by copy, the value (if any) is wrapped by an instance ofmeta_anythat directly refers to the actual element. Modifying it directly
modifies the element inside the container.The
insertmember function is used to add elements to a container. It gets
two arguments, the key and the value to insert:cppauto last = view.end(); // appends an integer to the container view.insert(last.handle(), 42, 'c');This function returns a boolean value to indicate whether the operation was
successful or not. A call toinsertmay fail when the arguments are not at
least convertible to the required types.The
erasemember function is used to remove elements from a container. It
gets a single argument, the key to remove:cppview.erase(42);This function returns a boolean value to indicate whether the operation was
successful or not. A call toerasemay fail when the argument is not at
least convertible to the required type.The
operator[]is used to access elements in a container. It gets a single
argument, the key of the element to return:cppentt::meta_any value = view[42];The function returns instances of
meta_anythat directly refer to the actual
elements. Modifying the returned object directly modifies the element inside
the container.
Container support is minimal but likely sufficient to satisfy all needs.
Pointer-like types
As with containers, it is also possible to tell to the meta system which types
are pointers. This makes it possible to dereference instances of meta_any,
thus obtaining light references to pointed objects that are also correctly
associated with their meta types.
To make the meta system recognize a type as pointer-like, users can specialize
the is_meta_pointer_like class. EnTT already exports the specializations for
some common classes. In particular:
- All types of raw pointers.
std::unique_ptrandstd::shared_ptr.- All classes that export a type member called
is_meta_pointer_like:The actual type is irrelevant and will not be used in any way.cppstruct smart_pointer { using is_meta_pointer_like = void; // ... };
It is important to include the header file pointer.hpp to make these
specializations available to the compiler when needed.
The same file also contains many examples for the users that are interested in
making their own pointer-like types available to the meta system.
When a type is recognized as a pointer-like one by the meta system, it is
possible to dereference the instances of meta_any that contain these objects.
The following is a deliberately verbose example to show how to use this feature:
int value = 42;
// meta type equivalent to that of int *
entt::meta_any any{&value};
if(any.type().is_pointer_like()) {
// meta type equivalent to that of int
if(entt::meta_any ref = *any; ref) {
// ...
}
}It is not necessary to perform a double check. Instead, it is enough to query
the meta type or verify that the returned object is valid. For example, invalid
instances are returned when the wrapped object is not a pointer-like type.
Dereferencing a pointer-like object returns an instance of meta_any which
refers to the pointed object. Modifying it means modifying the pointed object
directly (unless the returned element is const).
In general, dereferencing a pointer-like type boils down to a *ptr. However,EnTT also supports classes that do not offer an operator*. In particular:
It is possible to exploit a solution based on ADL lookup by implementing a
function (also a template one) nameddereference_meta_pointer_like:cpptemplate<typename Type> Type & dereference_meta_pointer_like(const custom_pointer_type<Type> &ptr) { return ptr.deref(); }When not in control of the type's namespace, it is possible to inject into the
enttnamespace a specialization of theadl_meta_pointer_likeclass
template to bypass the adl lookup as a whole:cpptemplate<typename Type> struct entt::adl_meta_pointer_like<custom_pointer_type<Type>> { static decltype(auto) dereference(const custom_pointer_type<Type> &ptr) { return ptr.deref(); } };
In all other cases and when dereferencing a pointer works as expected regardless
of the pointed type, no user intervention is required.
Template information
Meta types also provide a minimal set of information about the nature of the
original type in case it is a class template.
By default, this works out of the box and requires no user action. However, it
is important to include the header file template.hpp to make this information
available to the compiler when needed.
Meta template information is easily found:
// this method returns true if the type is recognized as a class template specialization
if(auto type = entt::resolve<std::shared_ptr<my_type>>(); type.is_template_specialization()) {
// meta type of the class template conveniently wrapped by entt::meta_class_template_tag
auto class_type = type.template_type();
// number of template arguments
std::size_t arity = type.template_arity();
// meta type of the i-th argument
auto arg_type = type.template_arg(0u);
}Typically, when template information for a type is required, what the library
provides is sufficient. However, there are some cases where a user may want more
details or a different set of information.
Consider the case of a class template that is meant to wrap function types:
template<typename>
struct function_type;
template<typename Ret, typename... Args>
struct function_type<Ret(Args...)> {};In this case, rather than the function type, it might be useful to provide the
return type and unpacked arguments as if they were different template parameters
for the original class template.
To achieve this, users must enter the library internals and provide their own
specialization for the class template entt::meta_template_traits, such as:
template<typename Ret, typename... Args>
struct entt::meta_template_traits<function_type<Ret(Args...)>> {
using class_type = meta_class_template_tag<function_type>;
using args_type = type_list<Ret, Args...>;
};The reflection system does not verify the accuracy of the information nor infer
a correspondence between real types and meta types.
Therefore, the specialization is used as is and the information it contains is
associated with the appropriate type when required.
Automatic conversions
In C++, there are a number of conversions allowed between arithmetic types that
make it convenient to work with this kind of data.
If this were to be translated into explicit registrations with the reflection
system, it would result in a long series of instructions such as the following:
entt::meta_factory<int>{}
.conv<bool>()
.conv<char>()
// ...
.conv<double>();Repeated for each type eligible to undergo this type of conversions. This is
both error-prone and repetitive.
Similarly, the language allows users to silently convert unscoped enums to their
underlying types and offers what it takes to do the same for scoped enums. It
would result in the following if it were to be done explicitly:
entt::meta_factory<my_enum>{}
.conv<std::underlying_type_t<my_enum>>();Fortunately, all of this can also be avoided. EnTT offers implicit support for
these types of conversions:
entt::meta_any any{42};
any.allow_cast<double>();
double value = any.cast<double>();With no need for registration, the conversion takes place automatically under
the hood. The same goes for a call to allow_cast involving a meta type:
entt::meta_type type = entt::resolve<int>();
entt::meta_any any{my_enum::a_value};
any.allow_cast(type);
int value = any.cast<int>();This makes working with arithmetic types and scoped or unscoped enums as easy as
it is in C++.
It is still possible to set up conversion functions manually, and these are
always preferred over the automatic ones.
Implicitly generated default constructor
Creating objects of default constructible types through the reflection system
while not having to explicitly register the meta type or its default constructor
is also possible.
For example, in the case of primitive types like int or char, but not just
them.
For default constructible types only, default constructors are automatically
defined and associated with their meta types, whether they are explicitly or
implicitly generated.
Therefore, this is all needed to construct an integer from its meta type:
entt::resolve<int>().construct();Where the meta type is, for example, the one returned from a meta container,
useful for building keys without knowing or having to register the actual types.
In all cases, when users register default constructors, they are preferred both
during searches and when the construct member function is invoked.
From void to any
Sometimes all a user has is an opaque pointer to an object of a known meta type.
It would be handy in this case to be able to construct a meta_any element from
it.
For this purpose, the meta_type class offers a from_void member function
designed to convert an opaque pointer into a meta_any:
entt::meta_any any = entt::resolve(id).from_void(element);Unfortunately, it is not possible to do a check on the actual type. Therefore,
this call can be considered as a static cast with all its problems.
On the other hand, the ability to construct a meta_any from an opaque pointer
opens the door to some pretty interesting uses that are worth exploring.
Policies: the more, the less
Policies are a kind of compile-time directives that can be used when registering
reflection information.
Their purpose is to require slightly different behavior than the default in some
specific cases. For example, when reading a given data member, its value is
returned wrapped in a meta_any object which, by default, makes a copy of it.
For large objects or if the caller wants to access the original instance, this
behavior is not desirable. Policies are there to offer a solution to this and
other problems.
There are a few alternatives available at the moment:
The as-value policy, associated with the type
entt::as_value_t.
This is the default policy. In general, it should never be used explicitly,
since it is implicitly selected if no other policy is specified.
In this case, the return values of the functions as well as the properties
exposed as data members are always returned by copy in a dedicated wrapper and
therefore associated with their original meta types.The as-void policy, associated with the type
entt::as_void_t.
Its purpose is to discard the return value of a meta object, whatever it is,
thus making it appear as if its type werevoid:cppentt::meta_factory<my_type>{}.func<&my_type::member_function, entt::as_void_t>("member"_hs);If the use with functions is obvious, perhaps less so is use with constructors
and data members. In the first case, the returned wrapper is always empty even
though the constructor is still invoked. In the second case, the property
is not accessible for reading instead.The as-ref and as-cref policies, associated with the types
entt::as_ref_tandentt::as_cref_t.
They allow to build wrappers that act as references to unmanaged objects.
Accessing the object contained in the wrapper for which the reference was
requested makes it possible to directly access the instance used to initialize
the wrapper itself:cppentt::meta_factory<my_type>{}.data<&my_type::data_member, entt::as_ref_t>("member"_hs);These policies work with constructors (for example, when objects are taken
from an external container rather than created on demand), data members and
functions in general.
If on the one handas_cref_talways forces the return type to be const,as_ref_tadapts to the constness of the passed object and to that of the
return type if any.The as-is policy, associated with the type
entt::as_is_t.
Useful for decoupling meta type creation code from calling code while still
preserving the behavior of data members and member functions as defined:cppentt::meta_factory<my_type>{}.func<&my_type::any_member, entt::as_is_t>("member"_hs);For data members or member functions that return a reference type, the value
is returned by reference with the same constness. In all other cases, the
value is returned by copy.
Some uses are rather trivial, but it is useful to note that there are some less
obvious corner cases that can in turn be solved with the use of policies.
Named constants and enums
As mentioned, the data member function is used to reflect constants of any
type.
This allows users to create meta types for enums that work exactly like any
other meta type built from a class. Similarly, arithmetic types are enriched
with constants of special meaning where required.
All values thus exported appear to users as if they were constant data members
of the reflected types. This avoids the need to export what is the difference
between enums and classes in C++ directly in the space of the reflected types.
Exposing constant values or elements from an enum is quite simple:
entt::meta_factory<my_enum>{}
.data<my_enum::a_value>("a_value"_hs)
.data<my_enum::another_value>("another_value"_hs);
entt::meta_factory<int>{}.data<2048>("max_int"_hs);Accessing them is trivial as well. It is a matter of doing the following, as
with any other data member of a meta type:
auto value = entt::resolve<my_enum>().data("a_value"_hs).get({}).cast<my_enum>();
auto max = entt::resolve<int>().data("max_int"_hs).get({}).cast<int>();All this happens behind the scenes without any allocation because of the small
object optimization performed by the meta_any class.
User defined data
Sometimes (for example, when it comes to creating an editor) it might be useful
to attach traits or arbitrary custom data to the meta objects created.
The main difference between them is that:
Traits are simple user-defined flags with much higher access performance. The
library reserves up to 16 bits for traits, that is 16 flags for a bitmask or
2^16 values otherwise.Custom data are stored in a generic quick access area reserved for the user
and which the library will never use under any circumstances.
In all cases, this support is currently available only for meta types, meta data
and meta functions.
Traits
User-defined traits are set via a meta factory:
entt::meta_factory<my_type>{}.traits(my_traits::required | my_traits::hidden);In the example above, EnTT bitmask enum support is used, but any integral
value is fine, as long as it does not exceed 16 bits.
Traits can be assigned at different times. Subsequent calls to the traits
function do not reset previously set values. However, users must reset the
factory to the meta object of interest:
entt::meta_factory<my_type>{}
.data<&my_type::data_member, entt::as_ref_t>("member"_hs)
.traits(my_traits::internal);Once created, all meta objects offer a member function named traits to get the
currently set value:
auto value = entt::resolve<my_type>().traits<my_traits>();Note that the type is erased upon registration and must therefore be repeated
when traits are extracted, so as to allow the library to reconstruct them
correctly.
Custom data
Custom arbitrary data are set via a meta factory:
entt::meta_factory<my_type>{}.custom<type_data>("name");The way to do this is by specifying the data type to the custom function and
passing the necessary arguments to construct it correctly.
It is not possible to assign custom data at different times. Therefore, multiple
calls to the custom function overwrite previous values. However, this value
can be read from meta objects and used to update existing data with a factory,
effectively updating them as needed.
Likewise, users can also set custom data on meta objects later if needed, as
long as the factory is reset to the meta object of interest:
entt::meta_factory<my_type>{}
.func<&my_type::member_function>("member"_hs)
.custom<function_data>("tooltip");Once created, all meta objects offer a member function named custom to get the
currently set value as a reference or as a pointer to an element:
const type_data &value = entt::resolve<my_type>().custom();Note that the returned object performs an extra check in debug before converting
to the requested type, so as to avoid subtle bugs.
Only in the case of conversion to a pointer is this check safe and such that a
null pointer is returned to inform the user of the failed attempt.
Unregister types
A type registered with the reflection system can also be unregistered. This
means unregistering all its data members, member functions, conversion functions
and so on. However, base classes are not unregistered as well, since they do not
necessarily depend on it.
Roughly speaking, unregistering a type means disconnecting all associated meta
objects from it and making its identifier no longer available:
entt::meta_reset<my_type>();It is also possible to reset types by their unique identifiers:
entt::meta_reset("my_type"_hs);Finally, there exists a non-template overload of the meta_reset function that
does not accept arguments and resets all meta types at once:
entt::meta_reset();A type can be re-registered later with a completely different name and form.
Meta context
All meta types and their parts are created at runtime and stored in a default
context. This is obtained via a service locator as:
auto &&context = entt::locator<entt::meta_context>::value_or();By itself, a context is an opaque object that the user can do little with.
However, users can replace an existing context with another at any time:
entt::meta_context other{};
auto &&context = entt::locator<entt::meta_context>::value_or();
std::swap(context, other);This is useful for testing purposes or to define multiple context objects with
different meta types to use as appropriate.
If replacing the default context is not enough, EnTT also offers the ability
to use multiple and externally managed contexts with the runtime reflection
system.
For example, to create new meta types within a context other than the default
one, simply pass it as an argument to the meta_factory constructor:
entt::meta_ctx context{};
entt::meta_factory<my_type>{context}.type("reflected_type"_hs);By doing so, the new meta type is not available in the default context but is
usable by passing around the new context when needed, such as when creating a
new meta_any object:
entt::meta_any any{context, std::in_place_type<my_type>};Similarly, to search for meta types in a context other than the default one,
it is necessary to pass it to the resolve function:
entt::meta_type type = entt::resolve(context, "reflected_type"_hs)More generally, when using externally managed contexts, it is always required to
provide the system with the context to use, at least at the entry point.
For example, once the meta_type instant is obtained, it is no longer necessary
to pass the context around as the meta type takes the information with it and
eventually propagates it to all its parts.
On the other hand, it is necessary to instruct the library on where meta types
are to be fetched when meta_anys and meta_handles are constructed, a factory
created or a meta type resolved.
Md Poly (`docs/md/poly.md`)
Crash Course: poly
Table of Contents
- Introduction
- Concept and implementation
- Inheritance
- Static polymorphism in the wild
- Storage size and alignment requirement
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 fulfill with concrete
classes without having to inherit from a common base.
Among others, this is one of the advantages of static polymorphism in general
and of a generic wrapper like that offered by the poly class template in
particular.
The result is an object to pass around as such and not through a reference or a
pointer, as it happens when it comes to working with dynamic polymorphism.
Since the poly class template makes use of entt::any internally, it also
supports most of its feature. For example, the possibility to create aliases to
existing and thus unmanaged objects. This allows users to exploit the static
polymorphism while maintaining ownership of 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.
The ones that I like more are:
dyno: runtime polymorphism done right.Poly:
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 have some doubts about the usefulness of some features in real world projects,
but perhaps my lack of experience comes into play here. In my opinion, its only
flaw is the API that 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.
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, it has some
limitations. It is 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, a generic implementation is needed to fulfill the
concept itself.
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 defined:
struct Drawable: entt::type_list<> {
template<typename Base>
struct type: Base {
void draw() { this->template invoke<0>(*this); }
};
// ...
};It is recognizable by the fact that it inherits from an empty type list.
Functions can also be const, accept any number of parameters and return a type
other than void:
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 are passed to invoke after the reference tothis and the return value is whatever the internal call returns.
As for invoke, this is a name that is injected into the concept throughBase, from which one must necessarily inherit. Since it is also a dependent
name, the this-> template form is unfortunately necessary due to the rules of
the language. However, there also exists an alternative that goes through an
external call:
struct Drawable: entt::type_list<> {
template<typename Base>
struct type: Base {
void draw() const { entt::poly_call<0>(*this); }
};
// ...
};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.
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:
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:
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?
In fact, this is the limitation that can be worked around by manually defining
the static virtual table.
When things are deduced, there is an implicit constraint.
If the concept exposes a member function called draw with function typevoid(), a concept is 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 is not possible to make use of functions not belonging to the
interface, even if they are part of the types that fulfill the concept.
Similarly, it is 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.
Fulfill a concept
The impl alias template of a concept is used to define how it is fulfilled:
struct Drawable: entt::type_list<> {
// ...
template<typename Type>
using impl = entt::value_list<&Type::draw>;
};In this case, it is stated that the draw method of a generic type is enough to
satisfy the requirements of the Drawable concept.
Both member functions and free functions are supported to fulfill concepts:
template<typename Type>
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 is erased
internally.
Moreover, the self parameter is not 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 is quite easy to build hierarchies of concepts if necessary.
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:
struct DrawableAndErasable: entt::type_list<> {
template<typename Base>
struct type: Drawable::type<Base> {
static constexpr auto base = Drawable::impl<Drawable::type<entt::poly_inspector>>::size;
void erase() { entt::poly_call<base + 0>(*this); }
};
template<typename Type>
using impl = entt::value_list_cat_t<
Drawable::impl<Type>,
entt::value_list<&Type::erase>
>;
};The static virtual table is empty and must remain so.
On the other hand, type no longer inherits from Base. Instead, it 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.
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, the list of types is extended in a similar
way to what is shown for the implementation of the above concept.
To do this, it is useful to declare a function that allows to convert a
concept into its underlying type_list object:
template<typename... Type>
entt::type_list<Type...> as_type_list(const entt::type_list<Type...> &);The definition is not strictly required, since the function is only used through
a decltype as it follows:
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.
Everything else is the same as already shown instead.
Static polymorphism in the wild
Once the concept and implementation are defined, it is possible to use thepoly class template to wrap instances that meet the requirements:
using drawable = entt::poly<Drawable>;
struct circle {
void draw() { /* ... */ }
};
struct square {
void draw() { /* ... */ }
};
// ...
drawable instance{circle{}};
instance->draw();
instance = square{};
instance->draw();This class offers a wide range of constructors, from the default one (which
returns an uninitialized poly object) to the copy and move constructors, as
well as the ability to create objects in-place.
Among others, there is also a constructor that allows users to wrap unmanaged
objects in a poly instance (either const or non-const ones):
circle shape;
drawable instance{std::in_place_type<circle &>, shape};Similarly, it is possible to create non-owning copies of poly from an existing
object:
drawable other = instance.as_ref();In both cases, although the interface of the poly object does not change, it
does not construct any element or take care of destroying the referenced
objects.
Note also how the underlying concept is accessed via a call to operator-> and
not directly as instance.draw().
This allows users to decouple the API of the wrapper from that of the concept.
Therefore, where instance.data() invokes the data member function of the
poly object, instance->data() maps directly to the functionality exposed by
the underlying concept.
Storage size and alignment requirement
Under the hood, the poly class template makes use of entt::any. Therefore,
it can take advantage of the possibility of defining at compile-time the size of
the storage suitable for the small buffer optimization as well as the alignment
requirements:
entt::basic_poly<Drawable, sizeof(double[4]), alignof(double[4])>The default size is sizeof(double[2]), which seems like a good compromise
between a buffer that is too large and one unable to hold anything larger than
an integer. The alignment requirement is optional, and by default such that it
is the most stringent (the largest) for any object whose size is at most equal
to the one provided.
It is worth noting that providing a size of 0 (which is an accepted value in all
respects) will force the system to dynamically allocate the contained objects in
all cases.
Md Process (`docs/md/process.md`)
Crash Course: cooperative scheduler
Table of Contents
Introduction
Processes are a useful tool to work around the strict definition of a system and
introduce logic in a different way, usually without resorting to other component
types.EnTT offers minimal support to this paradigm by introducing a few classes used
to define and execute cooperative processes.
The process
A typical task inherits from the process class template. Derived classes also
specify what the intended type for elapsed times is.
A process should implement the following member functions whether needed (note
that it is not required to define a function unless the derived class wants to
override the default behavior):
void update(Delta, void *);This is invoked once per tick until a process is explicitly aborted or ends
either with or without errors. Each process should at least define it to work
properly. Thevoid *parameter is an opaque pointer to user data (if any)
forwarded directly to the process during an update.void succeeded();This is invoked in case of success, immediately after an update and during the
same tick.void failed();This is invoked in case of errors, immediately after an update and during the
same tick.void aborted();This is invoked only if a process is explicitly aborted. There is no guarantee
that it executes in the same tick. It depends solely on whether the process is
aborted immediately or not.
A class can also change the state of a process by invoking succeed and fail,
as well as pause and unpause the process itself.
All these are public member functions made available to manage the life cycle of
a process easily.
Here is a minimal example for the sake of curiosity:
struct my_process: entt::process {
using allocator_type = entt::process::allocator_type;
using delta_type = entt::process::delta_type;
my_process(const allocator_type &allocator, delta_type delay)
: entt::process{allocator},
remaining{delay}
{}
void update(delta_type delta, void *) {
remaining -= std::min(remaining, delta);
// ...
if(!remaining) {
succeed();
}
}
private:
delta_type remaining;
};Continuation
A process may be followed by other processes upon successful termination.
This pairing can be set up at creation time, keeping the processes conceptually
separate from each other while still combining them at runtime:
my_process process{};
process.then<my_other_process>();This approach allows processes to be developed in isolation and combined to
define complex actions.
For example, a delayed operation where a parent process (such as a timer)
schedules a child process (the deferred task) once the time is over.
The then function also accepts lambdas, which are associated with a dedicated
process internally:
process.then([](entt::process &proc, std::uint32_t delta, void *data) {
// ...
})The lambda function is such that it accepts a reference to the process that
manages it (to be able to terminate it, pause it and so on), plus the usual
values also passed to the update function.
Shared process
All processes inherit from std::enable_shared_from_this to allow sharing with
the caller.
The returned smart pointer was created using the allocator associated with the
scheduler and therefore all its processes. This same allocator is available by
invoking get_allocator on the process itself.
As far as possible, sharing a process is not intended to allow the caller to
manage it. This could actually compromise the proper functioning of the
scheduler and the process itself.
Rather, the purpose is to allow the callers to save a valid reference to the
process, allowing them to intervene in its lifecycle through calls like pause
and the like.
The scheduler
A cooperative scheduler runs different processes and helps manage their life
cycles.
Each process is invoked once per tick. If it terminates, it is removed
automatically from the scheduler, and it is never invoked again. Otherwise,
it is a good candidate to run one more time the next tick.
A process can also have a child. In this case, the parent process is replaced
with its child when it terminates and only if it returns with success. In case
of errors, both the parent process and its child are discarded. This way, it is
easy to create a chain of processes to run sequentially.
Using a scheduler is straightforward. To create it, users must provide only the
type for the elapsed times and no arguments at all:
entt::basic_scheduler<std::uint64_t> scheduler;Otherwise, the scheduler alias is also available for the most common cases. It
uses std::uint32_t as a default type:
entt::scheduler scheduler{};The class has member functions to query its internal data structures, likeempty or size, as well as a clear utility to reset it to a clean state:
// checks if there are processes still running
const auto empty = scheduler.empty();
// gets the number of processes still running
entt::scheduler::size_type size = scheduler.size();
// resets the scheduler to its initial state and discards all the processes
scheduler.clear();To attach a process to a scheduler, invoke the attach function with a process
type and the arguments to use to construct it:
scheduler.attach<my_process>(_1000u);The scheduler will also provide the process with its allocator as the first
argument.
In case of lambdas or functors, the required signature is the one already seen
for the then function of a process:
scheduler.attach([](entt::process &, std::uint32_t, void *){ /* ... */ });In both cases, the newly created process is returned by reference and its then
member function is used to create chains of processes to run sequentially.
As a minimal example of use:
// schedules a task in the form of a lambda function
scheduler.attach([](entt::process &, std::uint32_t, void *) {
// ...
})
// appends a child in the form of another lambda function
.then([](entt::process &, std::uint32_t, void *) {
// ...
})
// appends a child in the form of a process class
.then<my_process>(1000u);To update a scheduler and therefore all its processes, the update member
function is the way to go:
// updates all the processes, no user data are provided
scheduler.update(delta);
// updates all the processes and provides them with custom data
scheduler.update(delta, &data);In addition to these functions, the scheduler offers an abort member function
that is used to discard all the running processes at once:
// aborts all the processes abruptly ...
scheduler.abort(true);
// ... or gracefully during the next tick
scheduler.abort();The argument passed to the abort function indicates whether execution should
be stopped immediately or processes should be notified on the next tick.
Md Reference (`docs/md/reference.md`)
Similar projects
Table of Contents
Introduction
There are many projects similar to EnTT, both open source and not.
Some even borrowed some ideas from this library and expressed them in different
languages.
Others developed different architectures from scratch and therefore offer
alternative solutions with their pros and cons.
If you know of other similar projects out there, feel free to open an issue or a
PR, and I will be glad to add them to this page.
I hope the following lists can grow much more in the future.
Similar projects
Below an incomplete list of similar projects that I have come across so
far.
If some terms or designs are not clear, I recommend referring to the
ECS Back and Forth series for all the
details.
C:
- destral_ecs: a single-file ECS based
on sparse sets. - Diana: an ECS that uses sparse sets to
keep track of entities in systems. - Flecs: a multithreaded archetype
ECS based on semi-contiguous arrays rather than chunks. - lent: the Donald Trump of the ECS libraries.
- destral_ecs: a single-file ECS based
C++:
- decs: a chunk-based archetype ECS.
- ecst: a multithreaded compile-time
ECS that uses sparse sets to keep track of entities in systems. - EntityX: a bitset based ECS that
uses a single large matrix of components indexed with entities. - Gaia-ECS: a chunk-based
archetype ECS. - Polypropylene: a hybrid
solution between an ECS and dynamic mixins.
C#
- Arch: a simple, fast and unity entities
inspired archetype ECS with optional multithreading. - Entitas: the ECS framework for
C# and Unity, where reactive systems were invented. - Fennecs: the little archetype ECS that
loves you back. - Friflo ECS: an archetype ECS
with focus on performance and minimal GC allocations. - LeoECS: simple lightweight C# Entity
Component System framework. - Massive ECS: sparse set based ECS
featuring rollbacks. - Svelto.ECS: a very interesting
platform-agnostic and table-based ECS framework.
- Arch: a simple, fast and unity entities
Go:
- gecs: a sparse sets based ECS inspired
byEnTT.
- gecs: a sparse sets based ECS inspired
Javascript:
- @javelin/ecs:
an archetype ECS in TypeScript. - ecsy: I have not had the time to
investigate the underlying design ofecsybut it looks cool anyway.
- @javelin/ecs:
Perl:
- Game::Entities: a simple
entity registry for ECS designs inspired byEnTT.
- Game::Entities: a simple
Raku:
- Game::Entities: a simple
entity registry for ECS designs inspired byEnTT.
- Game::Entities: a simple
Rust:
Zig
- zig-ecs: a zig-ification of
EnTT.
- zig-ecs: a zig-ification of
Md Resource (`docs/md/resource.md`)
Crash Course: resource management
Table of Contents
Introduction
Resource management is usually one of the most critical parts of a game.
Solutions are often tuned to the particular application. There exist several
approaches and all of them are perfectly fine as long as they fit the
requirements of the piece of software in which they are used.
Examples are loading everything on start, loading on request, predictive
loading, and so on.
EnTT does not pretend to offer a one-fits-all solution for the different
cases.
Instead, the library comes with a minimal, general purpose resource cache that
might be useful in many cases.
The resource, the loader and the cache
Resource, loader and cache are the three main actors for the purpose.
The resource is an image, an audio, a video or any other type:
struct my_resource { const int value; };The loader is a callable type, the aim of which is to load a specific
resource:
struct my_loader final {
using result_type = std::shared_ptr<my_resource>;
result_type operator()(int value) const {
// ...
return std::make_shared<my_resource>(value);
}
};Its function operator can accept any arguments and should return a value of the
declared result type (std::shared_ptr<my_resource> in the example).
A loader can also overload its function call operator to make it possible to
construct the same or another resource from different lists of arguments.
Finally, a cache is a specialization of a class template tailored to a specific
resource and (optionally) a loader:
using my_cache = entt::resource_cache<my_resource, my_loader>;
// ...
my_cache cache{};The class is designed to create different caches for different resource types
and to manage each one independently in the most appropriate way.
As a (very) trivial example, audio tracks can survive in most of the scenes of
an application while meshes can be associated with a single scene only, then
discarded when a player leaves it.
Resource handle
Resources are not returned directly to the caller. Instead, they are wrapped in
a resource handle, an instance of the entt::resource class template.
For those who know the flyweight design pattern already, that is exactly what
it is. To all others, this is the time to brush up on some notions instead.
A shared pointer could have been used as a resource handle. In fact, the default
implementation mostly maps the interface of its standard counterpart and only
adds a few things on top of it.
However, the handle in EnTT is designed as a standalone class template. This
is due to the fact that specializing a class in the standard library is often
undefined behavior while having the ability to specialize the handle for one,
more or all resource types could help over time.
Loaders
A loader is responsible for loading resources (quite obviously).
By default, it is just a callable object that forwards its arguments to the
resource itself. That is, a passthrough type. All the work is demanded to the
constructor(s) of the resource itself.
Loaders also are fully customizable as expected.
A custom loader is a class with at least one function call operator and a member
type named result_type.
The loader is not required to return a resource handle. As long as return_type
is suitable for constructing a handle, that is fine.
When using the default handle, it expects a resource type which is convertible
to or suitable for constructing an std::shared_ptr<Type> (where Type is the
actual resource type).
In other terms, the loader should return shared pointers to the given resource
type. However, this is not mandatory. Users can easily get around this
constraint by specializing both the handle and the loader.
A cache forwards all its arguments to the loader if required. This means that
loaders can also support tag dispatching to offer different loading policies:
struct my_loader {
using result_type = std::shared_ptr<my_resource>;
struct from_disk_tag{};
struct from_network_tag{};
template<typename... Args>
result_type operator()(from_disk_tag, Args&&... args) {
// ...
return std::make_shared<my_resource>(std::forward<Args>(args)...);
}
template<typename... Args>
result_type operator()(from_network_tag, Args&&... args) {
// ...
return std::make_shared<my_resource>(std::forward<Args>(args)...);
}
}This makes the whole loading logic quite flexible and easy to extend over time.
The cache class
The cache is the class that is asked to connect the dots.
It loads the resources, stores them aside and returns handles as needed:
entt::resource_cache<my_resource, my_loader> cache{};Under the hood, a cache is nothing more than a map where the key value has typeentt::id_type while the mapped value is whatever type its loader returns.
For this reason, it offers most of the functionalities a user would expect from
a map, such as empty or size and so on. Similarly, it is an iterable type
that also supports indexing by resource id:
for(auto [id, res]: cache) {
// ...
}
if(entt::resource<my_resource> res = cache["resource/id"_hs]; res) {
// ...
}Please, refer to the inline documentation for all the details about the other
functions (such as contains or erase).
Set aside the part of the API that this class shares with a map, it also adds
something on top of it in order to address the most common requirements of a
resource cache.
In particular, it does not have an emplace member function which is replaced
by load and force_load instead (where the former loads a new resource only
if not present while the second triggers a forced loading in any case):
auto ret = cache.load("resource/id"_hs);
// true only if the resource was not already present
const bool loaded = ret.second;
// takes the resource handle pointed to by the returned iterator
entt::resource<my_resource> res = ret.first->second;Note that the hashed string is used for convenience in the example above.
Resource identifiers are nothing more than integral values. Therefore, plain
numbers as well as non-class enum value are accepted.
It is worth mentioning that the iterators of a cache as well as its indexing
operators return resource handles rather than instances of the mapped type.
Since the cache has no control over the loader and a resource is not required to
also be convertible to bool, these handles can be invalid. This usually means an
error in the user logic, but it may also be an expected event.
It is therefore recommended to verify handles validity with a check in debug
(for example, when loading) or an appropriate logic in retail.
Md Signal (`docs/md/signal.md`)
Crash Course: events, signals and everything in between
Table of Contents
Introduction
Signals are more often than not a core part of games and software architectures
in general.
They help to decouple the various parts of a system while allowing them to
communicate with each other somehow.
The so-called modern C++ comes with a tool that can be useful in this regard,
the std::function. As an example, it can be used to create delegates.
However, there is no guarantee that an std::function does not perform
allocations under the hood and this could be problematic sometimes. Furthermore,
it solves a problem but may not adapt well to other requirements that may arise
from time to time.
In case that the flexibility and power of an std::function is not required or
if the price to pay for them is too high, EnTT offers a complete set of
lightweight classes to solve the same and many other problems.
Delegate
A delegate can be used as a general purpose invoker with no memory overhead for
free functions, lambdas and members provided along with an instance on which to
invoke them.
It does not claim to be a drop-in replacement for an std::function, so do not
expect to use it whenever an std::function fits well. That said, it is most
likely even a better fit than an std::function in a lot of cases, so expect to
use it quite a lot anyway.
The interface is trivial. It offers a default constructor to create empty
delegates:
entt::delegate<int(int)> delegate{};What is needed to create an instance is to specify the type of the function the
delegate accepts, that is the signature of the functions it models.
However, attempting to use an empty delegate by invoking its function call
operator results in undefined behavior or most likely a crash.
There exist a few overloads of the connect member function to initialize a
delegate:
int f(int i) { return i; }
struct my_struct {
int f(const int &i) const { return i; }
};
// bind a free function to the delegate
delegate.connect<&f>();
// bind a member function to the delegate
my_struct instance;
delegate.connect<&my_struct::f>(instance);The delegate class also accepts data members, if needed. In this case, the
function type of the delegate is such that the parameter list is empty and the
value of the data member is at least convertible to the return type.
Free functions having type equivalent to void(T &, args...) are accepted as
well. The first argument T & is considered a payload, and the function will
receive it back every time it is invoked. In other terms, this works just fine
with the above definition:
void g(const char &c, int i) { /* ... */ }
const char c = 'c';
delegate.connect<&g>(c);
delegate(42);Function g is invoked with a reference to c and 42. However, the function
type of the delegate is still void(int). This is also the signature of its
function call operator.
Another interesting aspect of the delegate class is that it accepts functions
with a list of parameters that is shorter than that of its function type:
void g() { /* ... */ }
delegate.connect<&g>();
delegate(42);Where the function type of the delegate is void(int) as above. It goes without
saying that the extra arguments are silently discarded internally. This is a
nice-to-have feature in a lot of cases, as an example when the delegate class
is used as a building block of a signal-slot system.
In fact, this filtering works both ways. The class tries to pass its first
count arguments first, then the last count. Watch out for conversion
rules if in doubt when connecting a listener!
Arbitrary functions that pull random arguments from the delegate list are not
supported instead. Other features were preferred, such as support for functions
with compatible argument lists although not equal to those of the delegate.
To create and initialize a delegate at once, there are a few specialized
constructors. Because of the rules of the language, the listener is provided by
means of the entt::connect_arg variable template:
entt::delegate<int(int)> func{entt::connect_arg<&f>};Aside connect, a disconnect counterpart is not provided. Instead, there
exists a reset member function to use to clear a delegate.
To know if a delegate is empty, it can be used explicitly in every conditional
statement:
if(delegate) {
// ...
}Finally, to invoke a delegate, the function call operator is the way to go as
already shown in the examples above:
auto ret = delegate(42);In all cases, listeners do not have to strictly follow the signature of the
delegate. As long as a listener can be invoked with the given arguments to yield
a result that is convertible to the given result type, everything works just
fine.
As a side note, members of classes may or may not be associated with instances.
If they are not, the first argument of the function type must be that of the
class on which the members operate, and an instance of this class must obviously
be passed when invoking the delegate:
entt::delegate<void(my_struct &, int)> delegate;
delegate.connect<&my_struct::f>();
my_struct instance;
delegate(instance, 42);In this case, it is not possible to deduce the function type since the first
argument does not necessarily have to be a reference (for example, it can be a
pointer, as well as a const reference).
Therefore, the function type must be declared explicitly for unbound members.
Runtime arguments
The delegate class is meant to be used primarily with template arguments.
However, as a consequence of its design, it also offers minimal support for
runtime arguments.
When used like this, some features are not supported though. In particular:
- Curried functions are not accepted.
- Functions with an argument list that differs from that of the delegate are not
supported. - Return type and types of arguments must coincide with those of the
delegate and being at least convertible is not enough anymore.
Moreover, for a given function type Ret(Args...), the signature of the
functions connected at runtime must necessarily be Ret(const void *, Args...).
Runtime arguments can be passed both to the constructor of a delegate and to theconnect member function. An optional parameter is also accepted in both cases.
This argument is used to pass arbitrary user data back and forth as aconst void * upon invocation.
To connect a function to a delegate in the hard way:
int func(const void *ptr, int i) { return *static_cast<const int *>(ptr) * i; }
const int value = 42;
// use the constructor ...
entt::delegate delegate{&func, &value};
// ... or the connect member function
delegate.connect(&func, &value);The type of the delegate is deduced from the function if possible. In this case,
since the first argument is an implementation detail, the deduced function type
is int(int).
Invoking a delegate built in this way follows the same rules as previously
explained.
Lambda support
In general, the delegate class does not fully support lambda functions in all
their nuances. The reason is pretty simple: a delegate is not a drop-in
replacement for an std::function. Instead, it tries to overcome the problems
with the latter.
That being said, non-capturing lambda functions are supported, even though some
features are not available in this case.
This is a logical consequence of the support for connecting functions at
runtime. Therefore, lambda functions undergo the same rules and
limitations.
In fact, since non-capturing lambda functions decay to pointers to functions,
they can be used with a delegate as if they were normal functions with
optional payload:
my_struct instance;
// use the constructor ...
entt::delegate delegate{+[](const void *ptr, int value) {
return static_cast<const my_struct *>(ptr)->f(value);
}, &instance};
// ... or the connect member function
delegate.connect([](const void *ptr, int value) {
return static_cast<const my_struct *>(ptr)->f(value);
}, &instance);As above, the first parameter (const void *) is not part of the function type
of the delegate and is used to dispatch arbitrary user data back and forth. In
other terms, the function type of the delegate above is int(int).
Raw access
While not recommended, a delegate also allows direct access to the stored
callable function target and underlying data, if any.
This makes it possible to bypass the behavior of the delegate itself and force
calls on different instances:
my_struct other;
delegate.target(&other, 42);It goes without saying that this type of approach is very risky, especially
since there is no way of knowing whether the contained function was originally a
member function of some class, a free function or a lambda.
Another possible (and meaningful) use of this feature is that of identifying a
particular delegate through its descriptive traits instead.
Signals
Signal handlers work with references to classes, function pointers, and pointers
to members. Listeners can be any kind of objects, and users are in charge of
connecting and disconnecting them from a signal to avoid crashes due to
different lifetimes. On the other side, performance should not be affected that
much by the presence of such a signal handler.
Signals make use of delegates internally, and therefore they undergo the same
rules and offer similar functionalities. It may be a good idea to consult the
documentation of the delegate class for further information.
A signal handler is can be used as a private data member without exposing any
publish functionality to the clients of a class.
The basic idea is to impose a clear separation between the signal itself and thesink class, that is a tool to be used to connect and disconnect listeners on
the fly.
The API of a signal handler is straightforward. If a collector is supplied to
the signal when something is published, all the values returned by its listeners
are literally collected and used later by the caller. Otherwise, the class
works just like a plain signal that emits events from time to time.
To create instances of signal handlers it is sufficient to provide the type of
function to which they refer:
entt::sigh<void(int, char)> signal;Signals offer all the basic functionalities required to know how many listeners
they contain (size) or if they contain at least a listener (empty), as well
as a function to use to swap handlers (swap).
Besides them, there are member functions to use both to connect and disconnect
listeners in all their forms by means of a sink:
void foo(int, char) { /* ... */ }
struct listener {
void bar(const int &, char) { /* ... */ }
};
// ...
entt::sink sink{signal};
listener instance;
sink.connect<&foo>();
sink.connect<&listener::bar>(instance);
// ...
// disconnects a free function
sink.disconnect<&foo>();
// disconnect a member function of an instance
sink.disconnect<&listener::bar>(instance);
// disconnect all member functions of an instance, if any
sink.disconnect(&instance);
// discards all listeners at once
sink.disconnect();As shown above, listeners do not have to strictly follow the signature of the
signal. As long as a listener can be invoked with the given arguments to yield a
result that is convertible to the given return type, everything works just
fine.
In all cases, the connect member function returns by default a connection
object to be used as an alternative to break a connection by means of itsrelease member function.
A scoped_connection can also be created from a connection. In this case, the
link is broken automatically as soon as the object goes out of scope.
Once listeners are attached (or even if there are no listeners at all), events
and data in general are published through a signal by means of the publish
member function:
signal.publish(42, 'c');To collect data, the collect member function is used instead:
int f() { return 0; }
int g() { return 1; }
// ...
entt::sigh<int()> signal;
entt::sink sink{signal};
sink.connect<&f>();
sink.connect<&g>();
std::vector<int> vec{};
signal.collect([&vec](int value) { vec.push_back(value); });
assert(vec[0] == 0);
assert(vec[1] == 1);A collector must expose a function operator that accepts as an argument a type
to which the return type of the listeners can be converted. Moreover, it can
optionally return a boolean value that is true to stop collecting data, false
otherwise. This way one can avoid calling all the listeners in case it is not
necessary.
Functors can also be used in place of a lambda. Since the collector is copied
when invoking the collect member function, std::ref is the way to go in this
case:
struct my_collector {
std::vector<int> vec{};
bool operator()(int v) {
vec.push_back(v);
return true;
}
};
// ...
my_collector collector;
signal.collect(std::ref(collector));Event dispatcher
The event dispatcher class allows users to trigger immediate events or to queue
and publish them all together later:
// define a general purpose dispatcher
entt::dispatcher dispatcher{};This class lazily instantiates its queues. Therefore, it is not necessary to
announce the event types in advance.
Connect, disconnect, publish
Listeners registered with a dispatcher are mainly of two types: free and member
functions. Lambdas as template functions are also accepted and belong to the
first group.
In all cases, a listener accepts an argument of type Event & for any type
of event, regardless of the return value.
Listeners are linked directly via connect to a sink object:
struct an_event { int value; };
struct another_event {};
void on_event(const an_event &event) { /* ... */ }
struct listener {
// Member function listener
void on_event(const another_event &) { /* ... */ }
};
// ...
// free function listener
dispatcher.sink<an_event>().connect<&on_event>();
listener listener;
// member function listener
dispatcher.sink<another_event>().connect<&listener::on_event>(listener);Note that connecting listeners within event handlers can result in undefined
behavior.
The disconnect member function is used to remove one listener at a time or all
of them at once:
// disconnects a free function
dispatcher.sink<an_event>().disconnect<&on_event>();
// disconnect a member function of an instance
dispatcher.sink<another_event>().disconnect<&listener::on_event>(listener);
// disconnect all member functions of an instance, if any
dispatcher.sink<another_event>().disconnect(&listener);The trigger member function serves the purpose of sending an immediate event
to all the listeners registered so far:
dispatcher.trigger(an_event{42});
dispatcher.trigger(another_event{});Listeners are invoked immediately, order of execution is not guaranteed. This
method can be used to push around urgent messages like an is terminating
notification on a mobile app.
On the other hand, the enqueue member function queues messages together and
helps to maintain control over the moment they are sent to listeners:
dispatcher.enqueue<an_event>(42);
dispatcher.enqueue(another_event{});Events are stored aside until the update member function is invoked:
// emits all the events of the given type at once
dispatcher.update<an_event>();
// emits all the events queued so far at once
dispatcher.update();This way users can embed the dispatcher in a loop and literally dispatch events
once per tick to their systems.
Named queues
All queues within a dispatcher are associated by default with an event type and
then retrieved from it.
However, it is possible to create queues with different names (and therefore
also multiple queues for a single type). In fact, more or less all functions
also take an additional parameter. As an example:
dispatcher.sink<an_event>("custom"_hs).connect<&listener::receive>(listener);In this case, the term name is misused as these are actual numeric identifiers
of type id_type.
An exception to this rule is the enqueue function. There is no additional
parameter for it but rather a different function:
dispatcher.enqueue_hint<an_event>("custom"_hs, 42);This is mainly due to the template argument deduction rules, and there is no
real (elegant) way to avoid it.
Event emitter
A general purpose event emitter thought mainly for those cases where it comes to
working with asynchronous stuff.
Originally designed to fit the requirements ofuvw (a wrapper for libuv written in
modern C++), it was adapted later to be included in this library.
To create an emitter type, derived classes must inherit from the base as:
struct my_emitter: emitter<my_emitter> {
// ...
}Handlers for the different events are created internally on the fly. It is not
required to specify in advance the full list of accepted events.
Moreover, whenever an event is published, an emitter also passes a reference
to itself to its listeners.
To create new instances of an emitter, no arguments are required:
my_emitter emitter{};Listeners are movable and callable objects (free functions, lambdas, functors,std::functions, whatever) whose function type is compatible with:
void(Type &, my_emitter &)Where Type is the type of event they want to receive.
To attach a listener to an emitter, there exists the on member function:
emitter.on<my_event>([](const my_event &event, my_emitter &emitter) {
// ...
});Similarly, the reset member function is used to disconnect listeners given a
type while clear is used to disconnect all listeners at once:
// resets the listener for my_event
emitter.erase<my_event>();
// resets all listeners
emitter.clear()To send an event to the listener registered on a given type, the publish
function is the way to go:
struct my_event { int i; };
// ...
emitter.publish(my_event{42});Finally, the empty member function tests if there exists at least a listener
registered with the event emitter while contains is used to check if a given
event type is associated with a valid listener:
if(emitter.contains<my_event>()) {
// ...
}This class introduces a nice-to-have model based on events and listeners.
More in general, it is a handy tool when the derived classes wrap asynchronous
operations, but it is not limited to such uses.
Md Stl (`docs/md/stl.md`)
Crash Course: User provided STL
Table of Contents
Introduction
EnTT internally decouples the standard template library from the std
namespace. It does this by introducing the internal stl namespace into which
the necessary tools are imported from std.
This import process is also configurable, and the end user can replace all or
some of the standard template library includes by providing their own
implementations.
The required tools
The list of what is imported from std is not fixed and stable over time, but
changes as the library evolves. At any time, users can easily know what is
required and what is not.
The easiest way to avoid errors is to provide everything a standard header
provides. However, it's also a good idea to look at its counterpart in the stl
folder and implement only the tools actually used by EnTT.
There's no guarantee that these won't change over time. Therefore, the first
approach is the safest way to avoid surprises.
STL injection
Includes from the standard template library can be injected into EnTT in
bulk or individually. As long as the API and guarantees remain the same, there
will be no problem using different sources or mixing different
implementations.
However, there are two fundamental requirements for the library to be able to
find the user files and use them correctly.
Discoverability
To begin with, the files to inject must be discoverable via __has_include in
the entt/ext/stl/ path, and have the extension .hpp.
For example, to replace ``, users need to provide a resolvable file such as<entt/ext/stl/bit.hpp>.
This is fairly simple to do on the command line and can be easily automated with
tools like CMake.
The EnTT test suite provides an example of how to proceed, also designed to
avoid future regressions.
Importability
Having all the required files is not enough, since EnTT does not know the
namespace in which the user defined their tools. Therefore, anyone providing the
new headers must also import all the tools into the entt::stl namespace.
Importing from outside into this namespace is accepted and, in a sense, expected
by the library itself, unlike what happens with the standard C++ library.
The easiest solution is to include a custom implementation and import it into
the required namespace via the files indicated.
An using namespace my_stl; directive should get the job done, but users can
also import the tools one by one if they prefer to.
Final note
EnTT is designed to work with the API, guarantees, and pros and cons of the
standard template library.
The flexibility afforded by the ability to import a custom implementation does
not compromise this requirement. Therefore, in the event of an incompatible API
(not different, but incompatible) or broken requirements, there is no
guarantee that EnTT will:
- Compile correctly
- Perform correctly
Obviously, any errors due to incorrect implementations or broken guarantees are
not attributable to the library, and no follow-up will be given to any issues
arising from these types of problems.
--- METRICS ---
- Files Extracted: 18
- Estimated Token Budget: ~76168 tokens
- Canonical Reference: https://codewiki.google/github.com/skypjack/entt