lib: standalone mode is now the default
This commit is contained in:
@@ -19,9 +19,8 @@
|
||||
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.<br/>
|
||||
Fortunately, nowadays using `EnTT` across boundaries is straightforward. In
|
||||
fact, everything just works transparently in almost all cases. There are only a
|
||||
few exceptions, easy to deal with anyway.
|
||||
Fortunately, nowadays using `EnTT` across boundaries is easier. However, use in
|
||||
standalone applications is favored and user intervention is otherwise required.
|
||||
|
||||
## The EnTT way
|
||||
|
||||
@@ -50,25 +49,17 @@ In general, these classes don't arouse much interest. The only exceptions are:
|
||||
* When working with plugins or shared libraries that don't export any symbol. In
|
||||
this case, `type_index` confuses the other classes by giving potentially wrong
|
||||
information to them.<br/>
|
||||
To avoid problems, it's required to provide a custom generator or to suppress
|
||||
the index generation as a whole:
|
||||
|
||||
```cpp
|
||||
template<typename Type>
|
||||
struct entt::type_index<Type> {};
|
||||
```
|
||||
|
||||
All classes that use `type_index` perform also a check on the possibility of
|
||||
creating indexes for types. If it's not a viable solution, they fallback on
|
||||
the type id provided by `type_info`. The latter makes everything stable across
|
||||
boundaries.<br/>
|
||||
This is why suppressing the generation of the indexes solves the problem. In
|
||||
case it's still necessary to associate sequential indexes with types, users
|
||||
can refer to the `family` class, although knowing that these will not be
|
||||
stable across boundaries.
|
||||
To avoid problems, it's required to provide a custom generator. Briefly, it's
|
||||
necessary to specialize the `type_index` class and make it point to a context
|
||||
that is also shared between the main application and the dynamically loaded
|
||||
libraries or plugins.<br/>
|
||||
This will make the type system shared and available to the whole application,
|
||||
not just to a particular tool such as the registry or the dispatcher. It means
|
||||
that a call to `type_index::id()` will return the same identifier for the same
|
||||
type from both sides of a boundary and can be used reliably for any purpose.
|
||||
|
||||
For anyone who needs more details, the test suite contains multiple examples
|
||||
covering the most common cases.<br/>
|
||||
covering the most common cases (see the `lib` directory for all details).<br/>
|
||||
It goes without saying that it's impossible to cover all the possible cases.
|
||||
However, what is offered should hopefully serve as a basis for all of them.
|
||||
|
||||
@@ -76,8 +67,8 @@ However, what is offered should hopefully serve as a basis for all of them.
|
||||
|
||||
The runtime reflection system deserves a special mention when it comes to using
|
||||
it across boundaries.<br/>
|
||||
Since it's linked to a static context to which the visible components are
|
||||
attached and different contexts don't relate to each other, they must be
|
||||
Since it's linked already to a static context to which the visible components
|
||||
are attached and different contexts don't relate to each other, they must be
|
||||
_shared_ to allow the use of meta types across boundaries.
|
||||
|
||||
Sharing a context is trivial though. First of all, the local one must be
|
||||
|
||||
@@ -70,11 +70,4 @@
|
||||
#endif
|
||||
|
||||
|
||||
#ifndef ENTT_STANDALONE
|
||||
# define ENTT_FAST_PATH(...) false
|
||||
#else
|
||||
# define ENTT_FAST_PATH(Cond) Cond
|
||||
#endif
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
@@ -105,39 +105,21 @@ class basic_registry {
|
||||
|
||||
template<typename Component>
|
||||
[[nodiscard]] const pool_t<Entity, Component> & assure() const {
|
||||
const sparse_set<entity_type> *curr;
|
||||
|
||||
if constexpr(ENTT_FAST_PATH(has_type_index_v<Component>)) {
|
||||
const auto index = type_index<Component>::value();
|
||||
|
||||
if(!(index < pools.size())) {
|
||||
pools.resize(size_type(index+1u));
|
||||
}
|
||||
|
||||
if(auto &&pdata = pools[index]; !pdata.pool) {
|
||||
pdata.type_id = type_info<Component>::id();
|
||||
pdata.pool.reset(new pool_t<Entity, Component>{});
|
||||
pdata.erase = +[](sparse_set<Entity> &cpool, basic_registry &owner, const Entity entt) {
|
||||
static_cast<pool_t<Entity, Component> &>(cpool).erase(owner, entt);
|
||||
};
|
||||
}
|
||||
|
||||
curr = pools[index].pool.get();
|
||||
} else {
|
||||
if(const auto it = std::find_if(pools.cbegin(), pools.cend(), [id = type_info<Component>::id()](const auto &pdata) { return id == pdata.type_id; }); it == pools.cend()) {
|
||||
curr = pools.emplace_back(pool_data{
|
||||
type_info<Component>::id(),
|
||||
std::unique_ptr<sparse_set<entity_type>>{new pool_t<Entity, Component>{}},
|
||||
+[](sparse_set<Entity> &cpool, basic_registry &owner, const Entity entt) {
|
||||
static_cast<pool_t<Entity, Component> &>(cpool).erase(owner, entt);
|
||||
}
|
||||
}).pool.get();
|
||||
} else {
|
||||
curr = it->pool.get();
|
||||
}
|
||||
const auto index = type_index<Component>::value();
|
||||
|
||||
if(!(index < pools.size())) {
|
||||
pools.resize(size_type(index)+1u);
|
||||
}
|
||||
|
||||
return *static_cast<const pool_t<Entity, Component> *>(curr);
|
||||
|
||||
if(auto &&pdata = pools[index]; !pdata.pool) {
|
||||
pdata.type_id = type_info<Component>::id();
|
||||
pdata.pool.reset(new pool_t<Entity, Component>());
|
||||
pdata.erase = +[](sparse_set<Entity> &cpool, basic_registry &owner, const Entity entt) {
|
||||
static_cast<pool_t<Entity, Component> &>(cpool).erase(owner, entt);
|
||||
};
|
||||
}
|
||||
|
||||
return static_cast<const pool_t<Entity, Component> &>(*pools[index].pool);
|
||||
}
|
||||
|
||||
template<typename Component>
|
||||
|
||||
@@ -39,6 +39,8 @@ class dispatcher {
|
||||
|
||||
template<typename Event>
|
||||
struct pool_handler final: basic_pool {
|
||||
static_assert(std::is_same_v<Event, std::decay_t<Event>>, "Invalid event type");
|
||||
|
||||
using signal_type = sigh<void(Event &)>;
|
||||
using sink_type = typename signal_type::sink_type;
|
||||
|
||||
@@ -90,24 +92,17 @@ class dispatcher {
|
||||
|
||||
template<typename Event>
|
||||
[[nodiscard]] pool_handler<Event> & assure() {
|
||||
static_assert(std::is_same_v<Event, std::decay_t<Event>>, "Invalid event type");
|
||||
|
||||
if constexpr(ENTT_FAST_PATH(has_type_index_v<Event>)) {
|
||||
const auto index = type_index<Event>::value();
|
||||
|
||||
if(!(index < pools.size())) {
|
||||
pools.resize(index+1u);
|
||||
}
|
||||
|
||||
if(!pools[index]) {
|
||||
pools[index].reset(new pool_handler<Event>{});
|
||||
}
|
||||
|
||||
return static_cast<pool_handler<Event> &>(*pools[index]);
|
||||
} else {
|
||||
auto it = std::find_if(pools.begin(), pools.end(), [id = type_info<Event>::id()](const auto &cpool) { return id == cpool->type_id(); });
|
||||
return static_cast<pool_handler<Event> &>(it == pools.cend() ? *pools.emplace_back(new pool_handler<Event>{}) : **it);
|
||||
const auto index = type_index<Event>::value();
|
||||
|
||||
if(!(index < pools.size())) {
|
||||
pools.resize(std::size_t(index)+1u);
|
||||
}
|
||||
|
||||
if(!pools[index]) {
|
||||
pools[index].reset(new pool_handler<Event>{});
|
||||
}
|
||||
|
||||
return static_cast<pool_handler<Event> &>(*pools[index]);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
@@ -50,6 +50,8 @@ class emitter {
|
||||
|
||||
template<typename Event>
|
||||
struct pool_handler final: basic_pool {
|
||||
static_assert(std::is_same_v<Event, std::decay_t<Event>>, "Invalid event type");
|
||||
|
||||
using listener_type = std::function<void(Event &, Derived &)>;
|
||||
using element_type = std::pair<bool, listener_type>;
|
||||
using container_type = std::list<element_type>;
|
||||
@@ -126,24 +128,17 @@ class emitter {
|
||||
|
||||
template<typename Event>
|
||||
[[nodiscard]] const pool_handler<Event> & assure() const {
|
||||
static_assert(std::is_same_v<Event, std::decay_t<Event>>, "Invalid event type");
|
||||
|
||||
if constexpr(ENTT_FAST_PATH(has_type_index_v<Event>)) {
|
||||
const auto index = type_index<Event>::value();
|
||||
|
||||
if(!(index < pools.size())) {
|
||||
pools.resize(index+1u);
|
||||
}
|
||||
|
||||
if(!pools[index]) {
|
||||
pools[index].reset(new pool_handler<Event>{});
|
||||
}
|
||||
|
||||
return static_cast<pool_handler<Event> &>(*pools[index]);
|
||||
} else {
|
||||
auto it = std::find_if(pools.begin(), pools.end(), [id = type_info<Event>::id()](const auto &cpool) { return id == cpool->type_id(); });
|
||||
return static_cast<pool_handler<Event> &>(it == pools.cend() ? *pools.emplace_back(new pool_handler<Event>{}) : **it);
|
||||
const auto index = type_index<Event>::value();
|
||||
|
||||
if(!(index < pools.size())) {
|
||||
pools.resize(std::size_t(index)+1u);
|
||||
}
|
||||
|
||||
if(!pools[index]) {
|
||||
pools[index].reset(new pool_handler<Event>{});
|
||||
}
|
||||
|
||||
return static_cast<pool_handler<Event> &>(*pools[index]);
|
||||
}
|
||||
|
||||
template<typename Event>
|
||||
|
||||
@@ -4,10 +4,16 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <entt/core/type_info.hpp>
|
||||
#include <entt/signal/dispatcher.hpp>
|
||||
#include "type_context.h"
|
||||
#include "types.h"
|
||||
|
||||
template<typename Type>
|
||||
struct entt::type_index<Type> {};
|
||||
struct entt::type_index<Type> {
|
||||
[[nodiscard]] static id_type value() ENTT_NOEXCEPT {
|
||||
static const entt::id_type value = type_context::instance()->value(entt::type_info<Type>::id());
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
struct listener {
|
||||
void on(message msg) { value = msg.payload; }
|
||||
@@ -23,8 +29,12 @@ TEST(Lib, Dispatcher) {
|
||||
dispatcher.sink<message>().connect<&listener::on>(listener);
|
||||
|
||||
cr_plugin ctx;
|
||||
ctx.userdata = &dispatcher;
|
||||
cr_plugin_load(ctx, PLUGIN);
|
||||
|
||||
ctx.userdata = type_context::instance();
|
||||
cr_plugin_update(ctx);
|
||||
|
||||
ctx.userdata = &dispatcher;
|
||||
cr_plugin_update(ctx);
|
||||
|
||||
ASSERT_EQ(listener.value, 42);
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
#include <cr.h>
|
||||
#include <entt/core/type_info.hpp>
|
||||
#include <entt/signal/dispatcher.hpp>
|
||||
#include "type_context.h"
|
||||
#include "types.h"
|
||||
|
||||
inline static type_context *context;
|
||||
|
||||
template<typename Type>
|
||||
struct entt::type_index<Type> {};
|
||||
struct entt::type_index<Type> {
|
||||
[[nodiscard]] static id_type value() ENTT_NOEXCEPT {
|
||||
static const entt::id_type value = context->value(type_info<Type>::id());
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
CR_EXPORT int cr_main(cr_plugin *ctx, cr_op operation) {
|
||||
switch (operation) {
|
||||
case CR_STEP:
|
||||
static_cast<entt::dispatcher *>(ctx->userdata)->trigger<event>();
|
||||
static_cast<entt::dispatcher *>(ctx->userdata)->trigger<message>(42);
|
||||
if(!context) {
|
||||
context = static_cast<type_context *>(ctx->userdata);
|
||||
} else {
|
||||
static_cast<entt::dispatcher *>(ctx->userdata)->trigger<event>();
|
||||
static_cast<entt::dispatcher *>(ctx->userdata)->trigger<message>(42);
|
||||
}
|
||||
break;
|
||||
case CR_CLOSE:
|
||||
case CR_LOAD:
|
||||
|
||||
28
test/lib/dispatcher_plugin/type_context.h
Normal file
28
test/lib/dispatcher_plugin/type_context.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef ENTT_LIB_DISPATCHER_PLUGIN_TYPE_CONTEXT_H
|
||||
#define ENTT_LIB_DISPATCHER_PLUGIN_TYPE_CONTEXT_H
|
||||
|
||||
#include <unordered_map>
|
||||
#include <entt/core/fwd.hpp>
|
||||
|
||||
class type_context {
|
||||
type_context() = default;
|
||||
|
||||
public:
|
||||
inline entt::id_type value(const entt::id_type name) {
|
||||
if(name_to_index.find(name) == name_to_index.cend()) {
|
||||
name_to_index[name] = entt::id_type(name_to_index.size());
|
||||
}
|
||||
|
||||
return name_to_index[name];
|
||||
}
|
||||
|
||||
static type_context * instance() {
|
||||
static type_context self{};
|
||||
return &self;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<entt::id_type, entt::id_type> name_to_index{};
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -4,10 +4,16 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <entt/core/type_info.hpp>
|
||||
#include <entt/signal/emitter.hpp>
|
||||
#include "type_context.h"
|
||||
#include "types.h"
|
||||
|
||||
template<typename Type>
|
||||
struct entt::type_index<Type> {};
|
||||
struct entt::type_index<Type> {
|
||||
[[nodiscard]] static id_type value() ENTT_NOEXCEPT {
|
||||
static const entt::id_type value = type_context::instance()->value(entt::type_info<Type>::id());
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(Lib, Emitter) {
|
||||
test_emitter emitter;
|
||||
@@ -18,8 +24,12 @@ TEST(Lib, Emitter) {
|
||||
emitter.once<message>([&](message msg, test_emitter &) { value = msg.payload; });
|
||||
|
||||
cr_plugin ctx;
|
||||
ctx.userdata = &emitter;
|
||||
cr_plugin_load(ctx, PLUGIN);
|
||||
|
||||
ctx.userdata = type_context::instance();
|
||||
cr_plugin_update(ctx);
|
||||
|
||||
ctx.userdata = &emitter;
|
||||
cr_plugin_update(ctx);
|
||||
|
||||
ASSERT_EQ(value, 42);
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
#include <cr.h>
|
||||
#include <entt/core/type_info.hpp>
|
||||
#include <entt/signal/emitter.hpp>
|
||||
#include "type_context.h"
|
||||
#include "types.h"
|
||||
|
||||
inline static type_context *context;
|
||||
|
||||
template<typename Type>
|
||||
struct entt::type_index<Type> {};
|
||||
struct entt::type_index<Type> {
|
||||
[[nodiscard]] static id_type value() ENTT_NOEXCEPT {
|
||||
static const entt::id_type value = context->value(type_info<Type>::id());
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
CR_EXPORT int cr_main(cr_plugin *ctx, cr_op operation) {
|
||||
switch (operation) {
|
||||
case CR_STEP:
|
||||
static_cast<test_emitter *>(ctx->userdata)->publish<event>();
|
||||
static_cast<test_emitter *>(ctx->userdata)->publish<message>(42);
|
||||
static_cast<test_emitter *>(ctx->userdata)->publish<message>(3);
|
||||
if(!context) {
|
||||
context = static_cast<type_context *>(ctx->userdata);
|
||||
} else {
|
||||
static_cast<test_emitter *>(ctx->userdata)->publish<event>();
|
||||
static_cast<test_emitter *>(ctx->userdata)->publish<message>(42);
|
||||
static_cast<test_emitter *>(ctx->userdata)->publish<message>(3);
|
||||
}
|
||||
break;
|
||||
case CR_CLOSE:
|
||||
case CR_LOAD:
|
||||
|
||||
28
test/lib/emitter_plugin/type_context.h
Normal file
28
test/lib/emitter_plugin/type_context.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef ENTT_LIB_EMITTER_PLUGIN_TYPE_CONTEXT_H
|
||||
#define ENTT_LIB_EMITTER_PLUGIN_TYPE_CONTEXT_H
|
||||
|
||||
#include <unordered_map>
|
||||
#include <entt/core/fwd.hpp>
|
||||
|
||||
class type_context {
|
||||
type_context() = default;
|
||||
|
||||
public:
|
||||
inline entt::id_type value(const entt::id_type name) {
|
||||
if(name_to_index.find(name) == name_to_index.cend()) {
|
||||
name_to_index[name] = entt::id_type(name_to_index.size());
|
||||
}
|
||||
|
||||
return name_to_index[name];
|
||||
}
|
||||
|
||||
static type_context * instance() {
|
||||
static type_context self{};
|
||||
return &self;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<entt::id_type, entt::id_type> name_to_index{};
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -13,8 +13,9 @@ TEST(Lib, Meta) {
|
||||
userdata ud{};
|
||||
|
||||
cr_plugin ctx;
|
||||
ctx.userdata = &ud;
|
||||
cr_plugin_load(ctx, PLUGIN);
|
||||
|
||||
ctx.userdata = &ud;
|
||||
cr_plugin_update(ctx);
|
||||
|
||||
entt::meta<double>().conv<int>();
|
||||
|
||||
@@ -13,8 +13,9 @@ TEST(Lib, Meta) {
|
||||
userdata ud{};
|
||||
|
||||
cr_plugin ctx;
|
||||
ctx.userdata = &ud;
|
||||
cr_plugin_load(ctx, PLUGIN);
|
||||
|
||||
ctx.userdata = &ud;
|
||||
cr_plugin_update(ctx);
|
||||
|
||||
entt::meta<double>().conv<int>();
|
||||
|
||||
@@ -4,31 +4,35 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <entt/core/type_info.hpp>
|
||||
#include <entt/entity/registry.hpp>
|
||||
#include "type_context.h"
|
||||
#include "types.h"
|
||||
|
||||
template<typename Type>
|
||||
struct entt::type_index<Type> {};
|
||||
struct entt::type_index<Type> {
|
||||
[[nodiscard]] static id_type value() ENTT_NOEXCEPT {
|
||||
static const entt::id_type value = type_context::instance()->value(entt::type_info<Type>::id());
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
TEST(Lib, Registry) {
|
||||
entt::registry registry;
|
||||
|
||||
for(auto i = 0; i < 3; ++i) {
|
||||
const auto entity = registry.create();
|
||||
registry.emplace<position>(entity, i, i);
|
||||
|
||||
if(i % 2) {
|
||||
registry.emplace<tag>(entity);
|
||||
}
|
||||
registry.emplace<position>(registry.create(), i, i);
|
||||
}
|
||||
|
||||
cr_plugin ctx;
|
||||
ctx.userdata = ®istry;
|
||||
cr_plugin_load(ctx, PLUGIN);
|
||||
|
||||
ctx.userdata = type_context::instance();
|
||||
cr_plugin_update(ctx);
|
||||
|
||||
ctx.userdata = ®istry;
|
||||
cr_plugin_update(ctx);
|
||||
|
||||
ASSERT_EQ(registry.size<position>(), registry.size<velocity>());
|
||||
ASSERT_NE(registry.size<position>(), registry.size());
|
||||
ASSERT_TRUE(registry.empty<tag>());
|
||||
ASSERT_EQ(registry.size<position>(), registry.size());
|
||||
|
||||
registry.view<position>().each([](auto entity, auto &position) {
|
||||
ASSERT_EQ(position.x, static_cast<int>(entt::to_integral(entity) + 16u));
|
||||
|
||||
@@ -1,28 +1,36 @@
|
||||
#include <cr.h>
|
||||
#include <entt/core/type_info.hpp>
|
||||
#include <entt/entity/registry.hpp>
|
||||
#include "type_context.h"
|
||||
#include "types.h"
|
||||
|
||||
inline static type_context *context;
|
||||
|
||||
template<typename Type>
|
||||
struct entt::type_index<Type> {};
|
||||
struct entt::type_index<Type> {
|
||||
[[nodiscard]] static id_type value() ENTT_NOEXCEPT {
|
||||
static const entt::id_type value = context->value(type_info<Type>::id());
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
CR_EXPORT int cr_main(cr_plugin *ctx, cr_op operation) {
|
||||
switch (operation) {
|
||||
case CR_STEP:
|
||||
[ctx]() {
|
||||
auto ®istry = *static_cast<entt::registry *>(ctx->userdata);
|
||||
|
||||
const auto position_view = registry.view<position>(entt::exclude<tag>);
|
||||
registry.insert(position_view.begin(), position_view.end(), velocity{1., 1.});
|
||||
|
||||
registry.view<position, velocity>().each([](auto &pos, auto &vel) {
|
||||
if(!context) {
|
||||
context = static_cast<type_context *>(ctx->userdata);
|
||||
} else {
|
||||
// forces things to break
|
||||
static_cast<entt::registry *>(ctx->userdata)->prepare<velocity>();
|
||||
|
||||
const auto view = static_cast<entt::registry *>(ctx->userdata)->view<position>();
|
||||
static_cast<entt::registry *>(ctx->userdata)->insert(view.begin(), view.end(), velocity{1., 1.});
|
||||
|
||||
static_cast<entt::registry *>(ctx->userdata)->view<position, velocity>().each([](auto &pos, auto &vel) {
|
||||
pos.x += static_cast<int>(16 * vel.dx);
|
||||
pos.y += static_cast<int>(16 * vel.dy);
|
||||
});
|
||||
|
||||
const auto tag_view = registry.view<tag>();
|
||||
registry.destroy(tag_view.begin(), tag_view.end());
|
||||
}();
|
||||
}
|
||||
break;
|
||||
case CR_CLOSE:
|
||||
case CR_LOAD:
|
||||
|
||||
28
test/lib/registry_plugin/type_context.h
Normal file
28
test/lib/registry_plugin/type_context.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef ENTT_LIB_REGISTRY_PLUGIN_TYPE_CONTEXT_H
|
||||
#define ENTT_LIB_REGISTRY_PLUGIN_TYPE_CONTEXT_H
|
||||
|
||||
#include <unordered_map>
|
||||
#include <entt/core/fwd.hpp>
|
||||
|
||||
class type_context {
|
||||
type_context() = default;
|
||||
|
||||
public:
|
||||
inline entt::id_type value(const entt::id_type name) {
|
||||
if(name_to_index.find(name) == name_to_index.cend()) {
|
||||
name_to_index[name] = entt::id_type(name_to_index.size());
|
||||
}
|
||||
|
||||
return name_to_index[name];
|
||||
}
|
||||
|
||||
static type_context * instance() {
|
||||
static type_context self{};
|
||||
return &self;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<entt::id_type, entt::id_type> name_to_index{};
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -11,6 +11,4 @@ struct velocity {
|
||||
double dy;
|
||||
};
|
||||
|
||||
struct tag {};
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user