doc: updated documentation (core, entity, poly)
This commit is contained in:
3
TODO
3
TODO
@@ -27,9 +27,6 @@ WIP:
|
||||
* HP: any/poly: configurable sbo size, compile-time policies like sbo-required.
|
||||
* HP: registry: use a poly object for pools, no more pool_data type.
|
||||
* HP: make runtime views use opaque storage and therefore return also elements.
|
||||
* HP: make try_get part of the storage adapter and use it in the poly storage.
|
||||
* HP: make poly storage optionally disabled
|
||||
* HP: poly support for data members and predefined vtable type (no need to adhere to the api of type)
|
||||
* suppress warnings in meta.hpp (uninitialized members)
|
||||
* add exclude-only views to combine with packs
|
||||
* deprecate non-owning groups in favor of owning views and view packs, introduce lazy owning views
|
||||
|
||||
@@ -270,10 +270,10 @@ object:
|
||||
|
||||
```cpp
|
||||
// aliasing constructor
|
||||
entt::any ref = other.ref();
|
||||
entt::any ref = as_ref(other);
|
||||
```
|
||||
|
||||
In this case, it doesn't matter if the starting container actually holds an
|
||||
In this case, it doesn't matter if the original container actually holds an
|
||||
object or acts already as a reference for unmanaged elements, the new instance
|
||||
thus created won't create copies and will only serve as a reference for the
|
||||
original item.<br/>
|
||||
@@ -305,11 +305,7 @@ A type info object is an opaque class that is also copy and move constructible.
|
||||
This class is returned by the `type_id` function template:
|
||||
|
||||
```cpp
|
||||
// generates an info object from a type ...
|
||||
auto info = entt::type_id<a_type>();
|
||||
|
||||
// ... or directly from a variable
|
||||
auto other = entt::type_id(42);
|
||||
```
|
||||
|
||||
These are the information made available by this object:
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
* [Context variables](#context-variables)
|
||||
* [Organizer](#organizer)
|
||||
* [Meet the runtime](#meet-the-runtime)
|
||||
* [Cloning a registry](#cloning-a-registry)
|
||||
* [Stamping an entity](#stamping-an-entity)
|
||||
* [Snapshot: complete vs continuous](#snapshot-complete-vs-continuous)
|
||||
* [Snapshot loader](#snapshot-loader)
|
||||
* [Continuous loader](#continuous-loader)
|
||||
@@ -880,153 +878,57 @@ use the preferred tool.
|
||||
|
||||
## Meet the runtime
|
||||
|
||||
Type identifiers are stable in `EnTT` during executions and most of the times
|
||||
also across different executions and across boundaries. This makes them suitable
|
||||
to mix runtime and compile-time features.<br/>
|
||||
The registry offers a function to _visit_ it and get the types of components it
|
||||
manages:
|
||||
`EnTT` takes full advantage of what the language offers at compile-time.<br/>
|
||||
However, by combining these feature with a tool for static polymorphism, it's
|
||||
also possible to have opaque proxies to work with _type-less_ pools at runtime.
|
||||
|
||||
These objects are returned by the `storage` member function, which accepts a
|
||||
`type_info` object as an argument rather than a compile-time type (the same
|
||||
returned by the `visit` member function):
|
||||
|
||||
```cpp
|
||||
registry.visit([](const auto component) {
|
||||
// ...
|
||||
auto storage = registry.storage(info);
|
||||
```
|
||||
|
||||
By default and to stay true with the philosophy of the library, the API of a
|
||||
proxy is minimal and doesn't allow users to do much.<br/>
|
||||
However, it's also completely customizable in a generic way and with the
|
||||
possibility of defining specific behaviors for given types.
|
||||
|
||||
This section won't go into detail on how to define a poly storage to get all the
|
||||
possible functionalities out of it. `EnTT` already contains enough snippets to
|
||||
get inspiration from, both in the test suite and in the `example` folder.<br/>
|
||||
In short, users will have to define their own _concepts_ (see the `entt::poly`
|
||||
documentation for this) and register them via the `poly_storage_traits` class
|
||||
template, which has been designed as sfinae-friendly for the purpose.
|
||||
|
||||
Once the concept that a poly storage must adhere to has been properly defined,
|
||||
copying an entity will be as easy as:
|
||||
|
||||
```cpp
|
||||
registry.visit(entity, [&](const auto info) {
|
||||
auto storage = registry.storage(info);
|
||||
storage->emplace(registry, other, storage->get(entity));
|
||||
});
|
||||
```
|
||||
|
||||
Moreover, there exists an overload to _visit_ a specific entity:
|
||||
Where `other` is the entity to which the elements should be replicated.<br/>
|
||||
Similarly, copying entire pools between different registries can look like this:
|
||||
|
||||
```cpp
|
||||
registry.visit(entity, [](const auto component) {
|
||||
// ...
|
||||
registry.visit([&](const auto info) {
|
||||
auto storage = registry.storage(info);
|
||||
other.storage(info)->insert(other, storage->data(), storage->raw(), storage->size());
|
||||
});
|
||||
```
|
||||
|
||||
This helps to create a bridge between the registry, that is heavily based on the
|
||||
C++ type system, and any other context where the compile-time isn't an option.
|
||||
For example: plugin systems, meta system, serialization, and so on.
|
||||
Where this time `other` represents the destination registry.
|
||||
|
||||
### Cloning a registry
|
||||
|
||||
Cloning a registry isn't a suggested practice since it could trigger many copies
|
||||
and cut down the performance. Moreover, because of how the `registry` class is
|
||||
designed, supporting this as a built-in feature would increase the compilation
|
||||
times also for the users that aren't interested in cloning. Even worse, it would
|
||||
make difficult to define different _cloning policies_ for different types when
|
||||
required.<br/>
|
||||
This is why function definitions for cloning have been moved to the user space.
|
||||
The `visit` member function of the `registry` class can help filling the gap,
|
||||
along with the `insert` functionality.
|
||||
|
||||
A general purpose cloning function could be defined as:
|
||||
|
||||
```cpp
|
||||
template<typename Type>
|
||||
void clone(const entt::registry &from, entt::registry &to) {
|
||||
const auto *data = from.data<Type>();
|
||||
const auto size = from.size<Type>();
|
||||
|
||||
if constexpr(ENTT_IS_EMPTY(Type)) {
|
||||
to.insert<Type>(data, data + size);
|
||||
} else {
|
||||
const auto *raw = from.raw<Type>();
|
||||
to.insert<Type>(data, data + size, raw, raw + size);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is probably the fastest method to inject entities and components in a
|
||||
registry that isn't necessarily empty. All new elements are _appended_ to the
|
||||
existing ones, if any.<br/>
|
||||
This function is also eligible for type erasure in order to create a mapping
|
||||
between type identifiers and opaque methods for cloning:
|
||||
|
||||
```cpp
|
||||
using clone_fn_type = void(const entt::registry &, entt::registry &);
|
||||
std::unordered_map<entt::id_type, clone_fn_type *> clone_functions;
|
||||
|
||||
// ...
|
||||
|
||||
clone_functions[entt::type_hash<position>::value()] = &clone<position>;
|
||||
clone_functions[entt::type_hash<velocity>::value()] = &clone<velocity>;
|
||||
```
|
||||
|
||||
Stamping a registry becomes straightforward with such a mapping then:
|
||||
|
||||
```cpp
|
||||
entt::registry from;
|
||||
entt::registry to;
|
||||
|
||||
// ...
|
||||
|
||||
from.visit([this, &to](const auto type_id) {
|
||||
clone_functions[type_id](from, to);
|
||||
});
|
||||
```
|
||||
|
||||
Custom cloning functions are also pretty easy to define. Moreover, also cloning
|
||||
registries specialized with different identifiers is possible this way.<br/>
|
||||
As a side note, cloning functions could be also attached to a reflection system
|
||||
where meta types are resolved using the runtime type identifiers.
|
||||
|
||||
### Stamping an entity
|
||||
|
||||
Using multiple registries at the same time is quite common. Examples are the
|
||||
separation of the UI from the simulation or the loading of different scenes in
|
||||
the background, possibly on a separate thread, without having to keep track of
|
||||
which entity belongs to which scene.<br/>
|
||||
In fact, with `EnTT` this is even a recommended practice, as the registry is
|
||||
nothing more than an opaque container you can swap at any time.
|
||||
|
||||
Once there are multiple registries available, one or more methods are needed to
|
||||
transfer information from one container to another though.<br/>
|
||||
This is where the `visit` member function of the `registry` class enters the
|
||||
game.
|
||||
|
||||
Since stamping a component could require different methods for different types
|
||||
and not all users want to benefit from this feature, function definitions have
|
||||
been moved from the registry to the user space.<br/>
|
||||
This helped to reduce compilation times and to allow for maximum flexibility,
|
||||
even though it requires users to set up their own stamping functions.
|
||||
|
||||
The best bet here is probably to define a reflection system or a mapping between
|
||||
the type identifiers and their opaque functions for stamping. As an example:
|
||||
|
||||
```cpp
|
||||
template<typename Type>
|
||||
void stamp(const entt::registry &from, const entt::entity src, entt::registry &to, const entt::entity dst) {
|
||||
to.emplace_or_replace<Type>(dst, from.get<Type>(src));
|
||||
}
|
||||
```
|
||||
|
||||
If the definition above is treated as a general purpose function for stamping,
|
||||
one can easily construct a map like the following one as a data member of a
|
||||
dedicate system:
|
||||
|
||||
```cpp
|
||||
using stamp_fn_type = void(const entt::registry &, const entt::entity, entt::registry &, const entt::entity);
|
||||
std::unordered_map<entt::id_type, stamp_fn_type *> stamp_functions;
|
||||
|
||||
// ...
|
||||
|
||||
stamp_functions[entt::type_hash<position>::value()] = &stamp<position>;
|
||||
stamp_functions[entt::type_hash<velocity>::value()] = &stamp<velocity>;
|
||||
```
|
||||
|
||||
Then _stamp_ entities across different registries as:
|
||||
|
||||
```cpp
|
||||
entt::registry from;
|
||||
entt::registry to;
|
||||
|
||||
// ...
|
||||
|
||||
from.visit(src, [this, &to, dst](const auto type_id) {
|
||||
stamp_functions[type_id](from, src, to, dst);
|
||||
});
|
||||
```
|
||||
|
||||
This way it's also pretty easy to define custom stamping functions for _special_
|
||||
types if needed. Moreover, stamping entities across registries specialized with
|
||||
different identifiers is possibile in practice.
|
||||
So, all in all, `EnTT` shifts the complexity to the one-time definition of a
|
||||
_concept_ that reflects the user's needs, and then leaves room for ease of use
|
||||
within the codebase.<br/>
|
||||
The possibility of extreme customization is the icing on the cake in this sense,
|
||||
allowing users to design this tool around their own requirements.
|
||||
|
||||
## Snapshot: complete vs continuous
|
||||
|
||||
|
||||
248
docs/md/poly.md
248
docs/md/poly.md
@@ -8,6 +8,10 @@
|
||||
* [Introduction](#introduction)
|
||||
* [Other libraries](#other-libraries)
|
||||
* [Concept and implementation](#concept-and-implementation)
|
||||
* [Deduced interface](#deduced-interface)
|
||||
* [Defined interface](#defined-interface)
|
||||
* [Fullfill a concept](#fullfill-a-concept)
|
||||
* [Inheritance](#inheritance)
|
||||
* [Static polymorphism in the wild](#static-polymorphism-in-the-wild)
|
||||
<!--
|
||||
@endcond TURN_OFF_DOXYGEN
|
||||
@@ -28,11 +32,10 @@ What users get is an object that can be passed around as such and not through a
|
||||
reference or a pointer, as happens when it comes to working with dynamic
|
||||
polymorphism.
|
||||
|
||||
Since the `poly` class template makes use of `entt::any` internally, it supports
|
||||
most of its features. Among the most important, the possibility to create
|
||||
aliases to existing objects and therefore not managed directly. This allows
|
||||
users to exploit the static polymorphism while maintaining ownership of their
|
||||
objects.<br/>
|
||||
Since the `poly` class template makes use of `entt::any` internally, it also
|
||||
supports most of its feature. Among the most important, the possibility to
|
||||
create aliases to existing and thus unmanaged objects. This allows users to
|
||||
exploit the static polymorphism while maintaining ownership of objects.<br/>
|
||||
Likewise, the `poly` class template also benefits from the small buffer
|
||||
optimization offered by the `entt::any` class and therefore minimizes the number
|
||||
of allocations, avoiding them altogether where possible.
|
||||
@@ -48,12 +51,12 @@ Among all, the two that I prefer are:
|
||||
object wrapper.
|
||||
|
||||
The former is admittedly an experimental library, with many interesting ideas.
|
||||
I've some doubts about the usefulness of some features in real world projects,
|
||||
but perhaps my ignorance comes into play here. In my opinion, its only flaw is
|
||||
the API which I find slightly more cumbersome than other solutions.<br/>
|
||||
I've some doubts about the usefulness of some feature in real world projects,
|
||||
but perhaps my lack of experience comes into play here. In my opinion, its only
|
||||
flaw is the API which I find slightly more cumbersome than other solutions.<br/>
|
||||
The latter was undoubtedly a source of inspiration for this module, although I
|
||||
opted for different choices in the implementation of both the final API and some
|
||||
features.
|
||||
feature.
|
||||
|
||||
Either way, the authors are gurus of the C++ community, people I only have to
|
||||
learn from.
|
||||
@@ -63,18 +66,49 @@ learn from.
|
||||
The first thing to do to create a _type-erasing polymorphic object wrapper_ (to
|
||||
use the terminology introduced by Eric Niebler) is to define a _concept_ that
|
||||
types will have to adhere to.<br/>
|
||||
In `EnTT`, this translates into the definition of a template class as follows:
|
||||
For this purpose, the library offers a single class that supports both deduced
|
||||
and fully defined interfaces. Although having interfaces deduced automatically
|
||||
is convenient and allows users to write less code in most cases, this has some
|
||||
limitations and it's therefore useful to be able to get around the deduction by
|
||||
providing a custom definition for the static virtual table.
|
||||
|
||||
Once the interface is defined, it will be sufficient to provide a generic
|
||||
implementation to fullfill the concept.<br/>
|
||||
Also in this case, the library allows customizations based on types or families
|
||||
of types, so as to be able to go beyond the generic case where necessary.
|
||||
|
||||
## Deduced interface
|
||||
|
||||
This is how a concept with a deduced interface is introduced:
|
||||
|
||||
```cpp
|
||||
template<typename Base>
|
||||
struct Drawable: Base {
|
||||
void draw() { this->template invoke<0>(*this); }
|
||||
struct Drawable: entt::type_list<> {
|
||||
template<typename Base>
|
||||
struct type: Base {
|
||||
void draw() { this->template invoke<0>(*this); }
|
||||
};
|
||||
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
The example is purposely minimal but the functions can receive values and return
|
||||
arguments. The former will be returned by the call to `invoke`, the latter must
|
||||
be passed to the same function after the reference to `this` instead.<br/>
|
||||
It's recognizable by the fact that it inherits from an empty type list.<br/>
|
||||
Functions can also be const, accept any number of paramters and return a type
|
||||
other than `void`:
|
||||
|
||||
```cpp
|
||||
struct Drawable: entt::type_list<> {
|
||||
template<typename Base>
|
||||
struct type: Base {
|
||||
bool draw(int pt) const { return this->template invoke<0>(*this, pt); }
|
||||
};
|
||||
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
In this case, all parameters must be passed to `invoke` after the reference to
|
||||
`this` and the return value is whatever the internal call returns.<br/>
|
||||
As for `invoke`, this is a name that is injected into the _concept_ through
|
||||
`Base`, from which one must necessarily inherit. Since it's also a dependent
|
||||
name, the `this-> template` form is unfortunately necessary due to the rules of
|
||||
@@ -82,43 +116,172 @@ the language. However, there exists also an alternative that goes through an
|
||||
external call:
|
||||
|
||||
```cpp
|
||||
template<typename Base>
|
||||
struct Drawable: Base {
|
||||
void draw() { entt::poly_call<0>(*this); }
|
||||
struct Drawable: entt::type_list<> {
|
||||
template<typename Base>
|
||||
struct type: Base {
|
||||
bool draw() const { entt::poly_call<0>(*this); }
|
||||
};
|
||||
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Once the _concept_ is defined, users need to specialize a template variable to
|
||||
tell the system how any type can satisfy its requirements:
|
||||
Once the _concept_ is defined, users must provide a generic implementation of it
|
||||
in order to tell the system how any type can satisfy its requirements. This is
|
||||
done via an alias template within the concept itself.<br/>
|
||||
The index passed as a template parameter to either `invoke` or `poly_call`
|
||||
refers to how this alias is defined.
|
||||
|
||||
## Defined interface
|
||||
|
||||
A fully defined concept is no different to one for which the interface is
|
||||
deduced, with the only difference that the list of types is not empty this time:
|
||||
|
||||
```cpp
|
||||
template<typename Type>
|
||||
inline constexpr auto entt::poly_impl<Drawable, Type> = entt::value_list<&Type::draw>{};
|
||||
struct Drawable: entt::type_list<void()> {
|
||||
template<typename Base>
|
||||
struct type: Base {
|
||||
void draw() { entt::poly_call<0>(*this); }
|
||||
};
|
||||
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Again, parameters and return values other than `void` are allowed. Also, the
|
||||
function type must be const when the method to bind to it is const:
|
||||
|
||||
```cpp
|
||||
struct Drawable: entt::type_list<bool(int) const> {
|
||||
template<typename Base>
|
||||
struct type: Base {
|
||||
bool draw(int pt) const { return entt::poly_call<0>(*this, pt); }
|
||||
};
|
||||
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Why should a user fully define a concept if the function types are the same as
|
||||
the deduced ones?<br>
|
||||
Because, in fact, this is exactly the limitation that can be worked around by
|
||||
manually defining the static virtual table.
|
||||
|
||||
When things are deduced, there is an implicit constraint.<br/>
|
||||
If the concept exposes a member function called `draw` with function type
|
||||
`void()`, a concept can be satisfied:
|
||||
|
||||
* Either by a class that exposes a member function with the same name and the
|
||||
same signature.
|
||||
|
||||
* Or through a lambda that makes use of existing member functions from the
|
||||
interface itself.
|
||||
|
||||
In other words, it's not possible to make use of functions not belonging to the
|
||||
interface, even if they are present in the types that fulfill the concept.<br/>
|
||||
Similarly, it's not possible to deduce a function in the static virtual table
|
||||
with a function type different from that of the associated member function in
|
||||
the interface itself.
|
||||
|
||||
Explicitly defining a static virtual table suppresses the deduction step and
|
||||
allows maximum flexibility when providing the implementation for a concept.
|
||||
|
||||
## Fullfill a concept
|
||||
|
||||
The `impl` alias template of a concept is used to define how it's fulfilled:
|
||||
|
||||
```cpp
|
||||
struct Drawable: entt::type_list<> {
|
||||
// ...
|
||||
|
||||
template<typename Type>
|
||||
using impl = entt::value_list<&Type::draw>;
|
||||
};
|
||||
```
|
||||
|
||||
In this case, it's stated that the `draw` method of a generic type will be
|
||||
enough to satisfy the requirements of the `Drawable` concept.<br/>
|
||||
The `poly_impl` variable template can be specialized in a generic way as in the
|
||||
example above, or for a specific type where this satisfies the requirements
|
||||
differently. Moreover, it's easy to specialize it for families of types:
|
||||
|
||||
```cpp
|
||||
template<typename Type>
|
||||
inline constexpr auto entt::poly_impl<Drawable, std::vector<Type>> = entt::value_list<&std::vector<Type>::size>{};
|
||||
```
|
||||
|
||||
Finally, an implementation doesn't have to consist of just member functions.
|
||||
Free functions are an alternative to fill any gaps in the interface of a type:
|
||||
Both member functions and free functions are supported to fullfill concepts:
|
||||
|
||||
```cpp
|
||||
template<typename Type>
|
||||
void print(Type &self) { self.print(); }
|
||||
|
||||
template<typename Type>
|
||||
inline constexpr auto entt::poly_impl<Drawable, Type> = entt::value_list<&print<Type>>{};
|
||||
struct Drawable: entt::type_list<void()> {
|
||||
// ...
|
||||
|
||||
template<typename Type>
|
||||
using impl = entt::value_list<&print<Type>>;
|
||||
};
|
||||
```
|
||||
|
||||
Refer to the variable template definition for more details.
|
||||
Likewise, as long as the parameter types and return type support conversions to
|
||||
and from those of the function type referenced in the static virtual table, the
|
||||
actual implementation may differ in its function type since it's erased
|
||||
internally.<br/>
|
||||
Moreover, the `self` parameter isn't strictly required by the system and can be
|
||||
left out for free functions if not required.
|
||||
|
||||
Refer to the inline documentation for more details.
|
||||
|
||||
# Inheritance
|
||||
|
||||
_Concept inheritance_ is straightforward due to how poly looks like in `EnTT`.
|
||||
Therefore, it's quite easy to build hierarchies of concepts if necessary.<br/>
|
||||
The only constraint is that all concepts in a hierarchy must belong to the same
|
||||
_family_, that is, they must be either all deduced or all defined.
|
||||
|
||||
For a deduced concept, inheritance is achieved in a few steps:
|
||||
|
||||
```cpp
|
||||
struct DrawableAndErasable: entt::type_list<> {
|
||||
template<typename Base>
|
||||
struct type: typename Drawable::type<Base> {
|
||||
static constexpr auto base = std::tuple_size_v<typename entt::poly_vtable<Drawable>::type>;
|
||||
void erase() { entt::poly_call<base + 0>(*this); }
|
||||
};
|
||||
|
||||
template<typename Type>
|
||||
using impl = entt::value_list_cat_t<
|
||||
typename Drawable::impl<Type>,
|
||||
entt::value_list<&Type::erase>
|
||||
>;
|
||||
};
|
||||
```
|
||||
|
||||
The static virtual table is empty and must remain so.<br/>
|
||||
On the other hand, `type` no longer inherits from `Base` and instead forwards
|
||||
its template parameter to the type exposed by the _base class_. Internally, the
|
||||
size of the static virtual table of the base class is used as an offset for the
|
||||
local indexes.<br/>
|
||||
Finally, by means of the `value_list_cat_t` utility, the implementation consists
|
||||
in appending the new functions to the previous list.
|
||||
|
||||
As for a defined concept instead, also the list of types must be extended, in a
|
||||
similar way to what is shown for the implementation of the above concept.<br/>
|
||||
To do this, it's useful to declare a function that allows to convert a _concept_
|
||||
into its underlying `type_list` object:
|
||||
|
||||
```cpp
|
||||
template<typename... Type>
|
||||
entt::type_list<Type...> as_type_list(const entt::type_list<Type...> &);
|
||||
```
|
||||
|
||||
The definition isn't strictly required, since the function will only be used
|
||||
through a `decltype` as it follows:
|
||||
|
||||
```cpp
|
||||
struct DrawableAndErasable: entt::type_list_cat_t<
|
||||
decltype(as_type_list(std::declval<Drawable>())),
|
||||
entt::type_list<void()>
|
||||
> {
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
Similar to above, `type_list_cat_t` is used to concatenate the underlying static
|
||||
virtual table with the new function types.<br/>
|
||||
Everything else is the same as already shown instead.
|
||||
|
||||
# Static polymorphism in the wild
|
||||
|
||||
@@ -140,10 +303,10 @@ struct square {
|
||||
// ...
|
||||
|
||||
drawable d{circle{}};
|
||||
d.draw();
|
||||
d->draw();
|
||||
|
||||
d = square{};
|
||||
d.draw();
|
||||
d->draw();
|
||||
```
|
||||
|
||||
The `poly` class template offers a wide range of constructors, from the default
|
||||
@@ -159,3 +322,10 @@ drawable d{std::ref(c)};
|
||||
|
||||
In this case, although the interface of the `poly` object doesn't change, it
|
||||
won't construct any element or take care of destroying the referenced object.
|
||||
|
||||
Note also how the underlying concept is accessed via a call to `operator->` and
|
||||
not directly as `d.draw()`.<br/>
|
||||
This allows users to decouple the API of the wrapper from that of the concept.
|
||||
Therefore, where `d.data()` will invoke the `data` member function of the poly
|
||||
object, `d->data()` will map directly to the functionality exposed by the
|
||||
underlying concept.
|
||||
|
||||
Reference in New Issue
Block a user