1

I'm working on a class representation utility that would work in a similar way to Java's Class class. That is, a mechanism that would emulate class reflection.

#include <map>
#include <stdexcept>
#include <string>

template<typename Class>
struct class_repr {

    std::map<std::string, uintptr_t> fields;
    std::map<std::string, void* (Class::*)(...)> methods;

    void declare_field(const std::string& name, void* pointer) {
        fields[name] = reinterpret_cast<uintptr_t>(pointer);
    }

    template<typename R, typename ...Params>
    void declare_instance_method(const std::string& name, R (Class::* pointer)(Params...)) {
        methods[name] = (void* (Class::*)(...)) pointer;
    }

    template<typename Tp>
    Tp& get_field(void* object, const std::string& name) {
        if (fields.count(name) == 0) throw std::invalid_argument("Field " + name + " not declared in the class descriptor");
        return *reinterpret_cast<Tp*>(uintptr_t(object) + fields.at(name));
    }

    template<typename R, typename ...Params>
    requires std::is_same_v<R, void>
    void invoke_instance_method(void* object, const std::string& name, Params&& ... params) {
        if (methods.count(name) == 0) throw std::invalid_argument("Method " + name + " not declared in the class descriptor");
        (reinterpret_cast<Class*>(object)->*methods.at(name))(std::forward<Params>(params)...);
    }

    template<typename R, typename ...Params>
    requires (not std::is_same_v<R, void>)
    R invoke_instance_method(void* object, const std::string& name, Params&& ... params) {
        if (methods.count(name) == 0) throw std::invalid_argument("Method " + name + " not declared in the class descriptor");
        return *static_cast<R*>((reinterpret_cast<Class*>(object)->*methods.at(name))(std::forward<Params>(params)...));
    }
};

And below is the class I'm testing it with:

#include <iostream>

class cat {

    std::string name, color;

    [[nodiscard]] const std::string& get_name() {
        return name;
    }

    [[nodiscard]] const std::string& get_color() {
        return color;
    }

    void say(std::string&& what) {
        std::cout << "[" << name << "]: " << what << std::endl;
    }

    void meow() {
        say("meow");
    }

    void say_color() {
        say("my fur is " + color);
    }

public:

    cat(std::string name, std::string color) : name(std::move(name)), color(std::move(color)) {}

    static class_repr<cat> get_representation() {
        class_repr<cat> descriptor;
        descriptor.declare_field("name", &(static_cast<cat*>(nullptr)->name));
        descriptor.declare_field("color", &(static_cast<cat*>(nullptr)->color));
        descriptor.declare_instance_method("get_name", &cat::get_name);
        descriptor.declare_instance_method("get_color", &cat::get_color);
        descriptor.declare_instance_method("say", &cat::say);
        descriptor.declare_instance_method("meow", &cat::meow);
        descriptor.declare_instance_method("say_color", &cat::say_color);
        return descriptor;
    }
};

This code works fine:

int main() {

    cat kitty("marble", "white");
    class_repr cat_class = cat::get_representation();

    cat_class.get_field<std::string>(&kitty, "name") = "skittle";
    cat_class.get_field<std::string>(&kitty, "color") = "gray";

    cat_class.invoke_instance_method<void>(&kitty, "meow");
    cat_class.invoke_instance_method<void>(&kitty, "say_color");
    std::cout << cat_class.invoke_instance_method<std::string>(&kitty, "get_name") << "'s color is indeed "
              << cat_class.invoke_instance_method<std::string>(&kitty, "get_color") << std::endl;

    return 0;
}

But when I try to call the say function, the code doesn't compile because non-primitive type objects cannot be passed through variadic method:

cat_class.invoke_instance_method<void, std::string&&>(&kitty, "say", "purr"); // error

Is there any way around making this work as intended (so that it calls an equivalent of kitty.say("purr"))?

14
  • warning: cast between incompatible pointer to member types from 'const std::__cxx11::basic_string<char>& (cat::*)()' to 'void* (cat::*)(...)' [-Wcast-function-type] methods[name] = (void* (Class::*)(...))pointer; Commented Nov 17, 2022 at 18:54
  • Are you trying to reimplement std::invoke? Commented Nov 17, 2022 at 18:55
  • @TedLyngmo are you compiling this code in C++20? Commented Nov 17, 2022 at 18:57
  • 1
    @Quimby it's not like OP does it any different Commented Nov 17, 2022 at 19:19
  • 1
    @Quimby just realize std::function doesn't help much, probably I was thinking unify pointer to member and pointer to member function that time. (&(static_cast<cat*>(nullptr)->name)) or std::function<void*(T*)> for parameterless function. Commented Nov 17, 2022 at 19:30

1 Answer 1

3

You can create a class representing any member function using type erasure (modified from this SO answer). No void*, no C-stype ellipsis ....

#include <memory>
#include <any>
#include <vector>
#include <functional>

class MemberFunction
{
public:

    template <typename R, typename C, typename... Args>
    MemberFunction(R (C::* memfunptr)(Args...))
    : type_erased_function{
        std::make_shared<Function<R, C, Args...>>(memfunptr)
    }
    {}

    template <typename R, typename C, typename... Args>
    R invoke(C* obj, Args&&... args){
        auto ret = type_erased_function->invoke(
            std::any(obj),
            std::vector<std::any>({std::forward<Args>(args)...})
        );
        if constexpr (!std::is_void_v<R>){
            return std::any_cast<R>(ret);
        }
    }

private:

    struct Concept {
        virtual ~Concept(){}
        virtual std::any invoke(std::any obj, std::vector<std::any> const& args) = 0;
    };

    template <typename R, typename C, typename... Args>
    class Function : public Concept
    {
    public:
        Function(R (C::* memfunptr)(Args...)) : func{memfunptr} {}

        std::any invoke(std::any obj, std::vector<std::any> const& args) override final
        {
            return invoke_impl(
                obj,
                args, 
                std::make_index_sequence<sizeof...(Args)>()
            );
        }
    private:

        template <size_t I>
        using Arg = std::tuple_element_t<I, std::tuple<Args...>>;

        template <size_t... I>
        std::any invoke_impl(std::any obj, std::vector<std::any> const& args, std::index_sequence<I...>)
        {
            auto invoke = [&]{ 
                return std::invoke(func, std::any_cast<C*>(obj), std::any_cast<std::remove_reference_t<Arg<I>>>(args[I])...); 
            };
            if constexpr (std::is_void_v<R>){
                invoke();
                return std::any();
            }
            else {
                return invoke();
            }
        }

        R (C::* func)(Args...);
    };

    std::shared_ptr<Concept> type_erased_function;

};

You store a std::map<std::string, MemberFunction> in your class_repr and change your declare_instance_method and invoke_instance_method like so:

template<typename R, typename ...Params>
void declare_instance_method(const std::string& name, R (Class::* pointer)(Params...)) {
    methods.insert({name, MemberFunction(pointer)});
}

template<typename R, typename ...Params>
requires std::is_same_v<R, void>
void invoke_instance_method(Class* object, const std::string& name, Params&& ... params) {
    if (methods.count(name) == 0) throw std::invalid_argument("Method " + name + " not declared in the class descriptor");
    methods.at(name).invoke<void>(object, std::forward<Params>(params)...);
}

template<typename R, typename ...Params>
requires (not std::is_same_v<R, void>)
R invoke_instance_method(Class* object, const std::string& name, Params&& ... params) {
    if (methods.count(name) == 0) throw std::invalid_argument("Method " + name + " not declared in the class descriptor");
    return methods.at(name).invoke<R>(object, std::forward<Params>(params)...);
}

Live Demo

Note that this is a prototype. To make this generally applicable you still need to invest quite a bit of work: You have to consider const member functions and const arguments, member functions mutating inputs or returning references etc. Also note, that std::any stores by value, so you might create some unnecessary copies of the function arguments.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.