veselink1 / refl-cpp

Static reflection for C++17 (compile-time enumeration, attributes, proxies, overloads, template functions, metaprogramming).
https://veselink1.github.io/refl-cpp/md__introduction.html
MIT License
1.05k stars 76 forks source link

Q: how to reflect on member functions' parameters and return types? #69

Closed RalphSteinhagen closed 2 years ago

RalphSteinhagen commented 2 years ago

We are using refl-cpp primarily for reflections on member fields which works perfectly fine for our use case.

We may have a new application now, where I was wondering whether this could be also extended also to member functions, notably to retrieve their parameter and return type signatures during compile time.

For example, having the following demo class definition:

class Circle {
    double r;
public:
    Circle(double val) : r(val) {}
    double getRadius() const { return r; }
    void setRadius(double radius) { r = radius; }
    std::string getFunc(int a, double b, std::string c) const { return fmt::format("{}{}{}",a,b,c); }
    double getDiameter() const { return 2 * r; }
    double getArea() const { return M_PI * r * r; }
};

How would one go about retrieving the setRadius(double) calling parameter (double) and return value (void)? Or, more generally, the number of calling parameters of getFunc(...) which would be std::tuple<int,double,std::string> and its return type std::string?

There is an example that illustrates how to get the function pointer and how to call 'std::invoke(..)` but this seems to implicitly assume the calling parameter and return types:

using refl::reflect;
using refl::util::find_one;
constexpr auto type = reflect<Circle>();

constexpr auto func = find_one(type.members, [](auto m) { return m.name == "getRadius"; }); // -> function_descriptor<Circle, 0>{...}

func.name; // -> const_string<6>{"getRadius"}
func.attributes; // -> std::tuple<>{}
func.is_resolved; // -> true
func.pointer; // -> pointer of type double (Circle::* const)()

using radius_t = double (Circle::* const)();
func.template resolve<radius_t>; // -> pointer of type radius_t on success, nullptr_t on fail.

Circle c(2.0);
func.invoke(c); // -> the result of c.getRadius()

Also more generally, how would one register, find and call the different class constructors and parameter signatures?

This (potentially new) functionality has applications in GUIs and other areas where the compile/run-time definitions are driven by configuration files.

Any help, suggestions, or feedback would be welcome. Thanks in advance.

veselink1 commented 2 years ago

Hey Ralph, function descriptors have a pointer field which is valid if the function is not overloaded (is_resolved is true). This is documented here: refl::descriptor::function_descriptor< T, N >::pointer.

One can write a template specialisation to extract the argument types from that:

template <typename...>
struct mem_fun_types;

template <typename C, typename R, typename... Args>
struct mem_fun_types<R(C::*)(Args...)> {
    using arg_types = type_list<Args...>;
    using return_type = R;
};

using pointer_type = decltype(func.pointer); //  -> pointer of type double (Circle::* const)()
using arg_types = typename mem_fun_types<pointer_type>::arg_types;

Note that you would need additional specialisations for the different possible cv-qualifiers on member functions (if they can be encountered in your codebase):

Constructors are not really typed as member functions so there isn't anything that can be done with them AFAIK. (One cannot take a pointer to a constructor.) You could use a custom attribute on the type itself to specify information about the constructor, but it might be helpful to first understand the exact use case.

Are you aware of this example here: https://github.com/veselink1/refl-cpp/blob/master/examples/example-binding.cpp It shows how a made-up GUI system could deserialize XML strings into objects using refl-cpp.

<StackPanel orientation="horizontal"> Hello, World! </StackPanel>

It wraps the deserialized output into an std::any (which can be downcast to a concrete type StackPanel).

RalphSteinhagen commented 2 years ago

Hey Vesko, thanks for the brilliant suggestion w.r.t. the 'template specialisation' trick.

Are these helper functions something that could be potentially also useful for others using refl-cpp?

I needed just a minor modification (R(C::*)(Args...) -> R(C::* const)(Args...)) and your works out-of-the-box:

template <typename C, typename R, typename... Args>
struct mem_fun_types<R (C::*const)(Args...)> {
    using arg_types = std::tuple<Args...>;
    using return_type = R;
};

For posterior -- in case, someone else stumbles across this question -- I made a more fleshed-out example on compiler-explorer to illustrate your proposal. For my use case this will do.

Will need to think about the constructors a bit more but it should also be solvable one way or the other.

Are you aware of this example here: https://github.com/veselink1/refl-cpp/blob/master/examples/example-binding.cpp

Yes, this is a similar motivating example. I mentioned 'GUI' because this is a use case that is more accessible for the general audience. My use-case is slightly different and non-UI related (i.e. dynamic parsing, modification, reinitialisation of GNU Radio flow graphs -> here).

Thanks again for your quick help and suggestions! :+1:

RalphSteinhagen commented 2 years ago

Will close this since my initial question has been answered.

veselink1 commented 2 years ago

Sorry about the typo. Thanks for linking a working example.

Are these helper functions something that could be potentially also useful for others using refl-cpp?

They could be useful, but I've refrained from adding them because they are available in other libraries in common use (e.g. function_traits in boost/type_traits).

FWIW, I don't think it is possible to deduce the argument types of constructors. This works for (member) functions because they are not overloaded and the compiler can deduce the type of the pointer to use in the expression (&Circle::getRadius), but if you think about it, constructors are always overloaded (because of implicitly generated constructors):

RalphSteinhagen commented 2 years ago

Sorry about the typo.

No worries, this just seems to confirm that you are human. :smile:

They could be useful, but I've refrained from adding them because [..] boost/type_traits.

I fully understand. Keeping the API envelope small eases maintenance.

FWIW, I don't think it is possible to deduce the argument types of constructors.

Yes and this isn't critical, because having one default constructor and one constructor with explicit custom attributes declared should work for the targeted application where these classes usually have only a limited number of constructors but potentially many member functions. The application is still in an explorative state but just wanted to check before investing too much in this with an MVP.

In any case, thanks a lot for your feedback!