Request: Support for auto-registered functions on meta
In C++, we normally have several tools to customize functions/"interfaces" for types (think of serialize/deserialize), which are most of the time defined at compile time. However, in gamedev specifically there is often a need to have customization for data which is loaded at runtime and is often type-erased, which is why EnTT has meta to help with this part. However, meta needs the user to specify the functions supported by a certain type, which is not optimal as you need to be explicit about everything. Furthermore, the registration might be cumbersome when a function is overloaded (think of ADL scenarios).
For this reason, also based on the recent discussion on imgui-based editors that use meta, I think it would be great if meta had a mechanism to automatically detect certain interfaces and automatically "register" them for each type supporting them. These interfaces don't need to be a fixed set and should probably be tied to a meta context. The idea would be to have something like the following:
using namespace entt::literals;
struct hash_interface {
template <typename T>
inline void operator()(void *type_) const {
auto &&type = *(entt::meta_factory<T> *)type_;
if constexpr (requires { std::hash<T>{}; }) // ok, std::hash is specialized for T, register hashing
type.func<&std::hash<T>::operator()>("hash"_hs);
}
};
struct foo
{
int a;
char b;
float c;
};
// dummy setup
template <>
struct std::hash<foo>
{
inline std::size_t operator()(foo const &) const { return 42; }
};
int main()
{
entt::locator<entt::meta_ctx>::value_or()
.register_auto_interface<hash_interface>(); // ok: hash_interface will be registered for all types that support it
entt::meta_factory<foo>{} // all registered interfaces are checked here, in this case only `hash_interface`
.data<&foo::a>("a"_hs)
.data<&foo::b>("b"_hs)
.data<&foo::c>("c"_hs);
foo f{.a = 10, .b = 'b', .c = 0.1f};
entt::meta_any any_foo{f};
auto hash = entt::resolve<foo>().func("hash"_hs);
// prints 42
std::cout << hash(any_foo); // I can't remember if this is the syntax, but you get the idea
}
How would the register_auto_interface function work though? I think the template token here makes it tricky to get it done:
template <typename T>
inline void operator()(void *type_) const;
We have two different layers of template machinery in this case: one when you register the interfaces, the other one on the intefaces. I don't see a clear way to make it work, but I'm open to suggestions to push the discussion further. 🙂
There is no way to register functions on a specific class at runtime without specifying all the intervfaces at compile time
entt::locator<entt::meta_ctx>::value_or()
.register_auto_interface<hash_interface>(); // ok: hash_interface will be registered for all types that support it
needs to return a type that holds all template<class... Interfaces> . then you register by calling on that return value the functions
int main()
{
auto locator = entt::locator<entt::meta_ctx>::value_or()
.register_auto_interface<hash_interface>(); // ok: hash_interface will be registered for all types that support it
// have to register all states at this call by chaining maybe?
// passing locator
entt::meta_factory<foo>{locator} // all registered interfaces are checked here, in this case only `hash_interface`
.data<&foo::a>("a"_hs)
.data<&foo::b>("b"_hs)
.data<&foo::c>("c"_hs);
foo f{.a = 10, .b = 'b', .c = 0.1f};
entt::meta_any any_foo{f};
auto hash = entt::resolve<foo>().func("hash"_hs);
// prints 42
std::cout << hash(any_foo); // I can't remember if this is the syntax, but you get the idea
}
you are essentially trying to solve the issue of arbitary std::any visitation without specifying all the interfaces to visit which is impossible.
I know you could get close with Stateful meta programming but I wouldn't like to see that used in a library as it is inconsistent and not portable
Then why not an ADL-solved invocation from within meta_factory constructor or the like, on functions that can use concepts to filter their arguments and create bindings automatically? I think it would work as well, just without the register_auto_interface step. Makes sense?
Then why not an ADL-solved invocation from within
meta_factoryconstructor or the like, on functions that can use concepts to filter their arguments and create bindings automatically? I think it would work as well, just without theregister_auto_interfacestep. Makes sense?
ADL is Arguement-Dependant-Lookup and if meta_factory contains no arguement how will it know what to call? my example passes the locator to it so it can know is this what you mean?
First of all, my idea was to have a dense_set<interface_fptr> inside meta_ctx but like you said, I just realized you can't fully type-erase the function signature. 😅
Then why not an ADL-solved invocation from within meta_factory constructor or the like, on functions that can use concepts to filter their arguments and create bindings automatically? I think it would work as well, just without the register_auto_interface step. Makes sense?
That would work actually, I think it's cleaner/less error prone (?) + simpler to write overall. Maybe not an ADL solution, but a simple type trait would do I think.
// default implementation
template <typename>
struct inject_interfaces {
template <typename T>
inline static void inject(entt::meta_factory<T> &type) {} // do nothing by default
};
// specialize inject_interfaces<void> if you want your own implementation
template <>
struct inject_interfaces<void> {
template <typename T>
inline static void inject(entt::meta_factory<T> &type) {
// similar to the hash_interface example earlier
}
};
Also another alternative I was thinking of is to simply have functions that return the meta_factory<T> for you, but I don't think that composes well...
// how do you compose this with other traits?
template <typename T>
auto meta_hashable() {
if constexpr (is-hashable<T>) {
return entt::meta_factory<T>{}.func<&hash-func>("hash"_hs);
} else {
return entt::meta_factory<T>{};
}
}
First of all, my idea was to have a
dense_set<interface_fptr>insidemeta_ctxbut like you said, I just realized you can't fully type-erase the function signature. 😅Then why not an ADL-solved invocation from within meta_factory constructor or the like, on functions that can use concepts to filter their arguments and create bindings automatically? I think it would work as well, just without the register_auto_interface step. Makes sense?
That would work actually, I think it's cleaner/less error prone (?) + simpler to write overall. Maybe not an ADL solution, but a simple type trait would do I think.
// default implementation template <typename> struct inject_interfaces { template <typename T> inline static void inject(entt::meta_factory<T> &type) {} // do nothing by default }; // specialize inject_interfaces<void> if you want your own implementation template <> struct inject_interfaces<void> { template <typename T> inline static void inject(entt::meta_factory<T> &type) { // similar to the hash_interface example earlier } };Also another alternative I was thinking of is to simply have functions that return the
meta_factory<T>for you, but I don't think that composes well...// how do you compose this with other traits? template <typename T> auto meta_hashable() { if constexpr (is-hashable<T>) { return entt::meta_factory<T>{}.func<&hash-func>("hash"_hs); } else { return entt::meta_factory<T>{}; } }
but wouldn't limitting it to 1 specilization disallow you to overload it if it is already overloaded by some other library?
Yes, that's a problem. For me you don't want to do that either way because afaik it can and will cause ODR violations (ie. having specializations of a type/function and pick the "most recent" one). Maybe libraries should have something like a "register_traits" which you can/should call manually instead? I'm open to suggestions though, so let me know if you have a better idea.
Yes, that's a problem. For me you don't want to do that either way because afaik it can and will cause ODR violations (ie. having specializations of a type/function and pick the "most recent" one). Maybe libraries should have something like a "register_traits" which you can/should call manually instead? I'm open to suggestions though, so let me know if you have a better idea.
did you see my code I posted about returning a local variable?
Yes, what about it?
What I meant is this, that is, a straightforward mechanism that allows users to define meta setup functions in user space. With concepts they are waaaaaay nicer than what I did in the tests too. Roughly speaking, this isn't even required, because the following achieves the same:
entt::meta_factory<MyType> factory{};
meta_setup(factory); // defined in user space
factory.do_something_else();
However, the factory.setup().do_something_else() feels nicer to me. Since it's also easy to maintain overall, why not. 🙂
Makes sense? Thoughts? It's on the wip branch, we've plenty of time to refine or drop it eventually. 👍
Ok, now I see what you meant with ADL. Seems we are on the same page on what it should look like then, but I still have one last thing to ask.
Why should .setup() be a separate function? Wouldn't it be better if you just call meta_setup(*this) from within the constructor of meta_factory (if meta_setup exists/is found) instead? What am I missing here? 🤔
Yes and no. With the setup function you decide. With an implicit call from the constructor, you don't decide.
If I want to introduce a fallback function that asserts when a setup function does not exist, it could cause problems in the second case.
Maybe. I guess one can get around it somehow, but you know, I'm all about being explicit and in control rather than voted to hidden implicit calls. 🙂
That said, I reverted the ADL changes because they failed and I didn't get why. 🤔 Any suggestions? I'm probably missing something obvious with them... an extra pair of 👀 would help.
Well not exactly ADL, but you can get it to work if you specialize meta_setup inside entt. Here's a link to show what I mean. My understanding is that meta_factory is an EnTT type, so meta_setup is looked up within the parent namespace, that is, entt. So the user would have to overload entt::meta_setup unless there is some parameter from another namespace passed to meta_setup.
I'm all about being explicit and in control rather than voted to hidden implicit calls. 🙂
Ok, I see. I would still argue to at least have some tag that says "call this setup automatically in the constructor for me". Something in the lines of:
// user should write this
void meta_setup(entt::meta_factory<foo> &factory, entt::auto_setup_t) { /* ... */ }
// then on meta_factory<T> constructor
if constexpr (requires { meta_setup(*this, auto_setup_t{}); }) {
meta_setup(*this, auto_setup_t{});
}
Let me know what you think.
Well not exactly ADL, but you can get it to work if you specialize
meta_setupinsideentt. Here's a link to show what I mean. My understanding is thatmeta_factoryis an EnTT type, someta_setupis looked up within the parent namespace, that is,entt. So the user would have to overloadentt::meta_setupunless there is some parameter from another namespace passed tometa_setup.I'm all about being explicit and in control rather than voted to hidden implicit calls. 🙂
Ok, I see. I would still argue to at least have some tag that says "call this setup automatically in the constructor for me". Something in the lines of:
// user should write this void meta_setup(entt::meta_factory<foo> &factory, entt::auto_setup_t) { /* ... */ } // then on meta_factory<T> constructor if constexpr (requires { meta_setup(*this, auto_setup_t{}); }) { meta_setup(*this, auto_setup_t{}); }Let me know what you think.
the issue I see is with UB because what if the user doesn't include this overload with their class type? it will cause ODR voilations.
I don't hace an idea to prevent it....
I think that happens with specializing traits too, right? There's no good way around it as far as I can tell neither unless we have meta_setup work as following:
struct foo {
static void meta_setup(entt::meta_factory<foo> &factory) {
// ...
}
};
This is similar to on_construct & such on auto signals side, but the problem is it doesn't work for types not owned by you, in which case you're forced with a function or trait.
Soooooo 😅 I want to address all pending requests, but I don't know what we expect to do here actually. The more I read it, the more it seems to me that we can already have this on top of meta. Am I missing something?
I guess what I wanted was to have some kind of "open constructor" for entt::meta_factory<T> where we as users can do different things for different T (eg. register .print() for some T and .draw() for others, etc.). An option we discussed is to have this meta_setup function that registers some functions that might be available. In practice users would have just one "customization" of this function and it would look like this:
template <typename T>
inline void meta_setup(entt::meta_factory<T> &factory) {
using namespace entt::literals;
// T might be printable
if constexpr (printable<T>) {
factory.func<&T::print>("print"_hs);
}
// or T might be drawable
if constexpr (drawable<T>) {
factory.func<&T::draw>("draw"_hs);
}
// or maybe something else...
}
My problem with this is that calling this "setup" is verbose, even with meta_factory.setup(), which I think defeats the purpose of the function itself, as users still have to call this. To my idea, the optimal solution would be to not have .setup() at all and instead opt to check types on meta_setup to skip "auto-registering" by doing a type_list::contains or maybe by having some T::no_meta_setup alias. Either way, the choice for how to do is explicit and remains to the user, but the call to meta_setup would be implicit in itself.
Maybe you thought of something simpler, I'm still open to suggestions. 😅