Перенаправить вызов метода экземпляра с переменным числом аргументов через указатель на функцию-член в C++

Я работаю над утилитой представления классов, которая будет работать аналогично классу Class в Java. То есть механизм, который бы эмулировал отражение классов.

#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)...));
    }
};

И ниже класс, с которым я его тестирую:

#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;
    }
};

Этот код работает нормально:

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;
}

Но когда я пытаюсь вызвать функцию say, код не компилируется, потому что объекты непримитивного типа нельзя передать через вариативный метод:

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

Есть ли способ заставить это работать так, как задумано (чтобы он вызывал эквивалент kitty.say("purr"))?

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;
Ted Lyngmo 17.11.2022 19:54

Вы пытаетесь заново реализовать std::invoke?

fabian 17.11.2022 19:55

@TedLyngmo, вы компилируете этот код на С++ 20?

RubyNaxela 17.11.2022 19:57
&(static_cast<cat*>(nullptr)->name) это не очень хорошая идея.
Quimby 17.11.2022 19:57

@Quimby этот код не предназначен для переноса, он должен работать под Clang 13, что он и делает.

RubyNaxela 17.11.2022 20:00

@RubyNaxela Да, C++20 в g++, clang++ и MSVC. g++ и clang++ выдают похожие сообщения. Однако MSVC не жалуется.

Ted Lyngmo 17.11.2022 20:09

@RubyNaxela Хорошо, вы можете по крайней мере исправить с помощью offsetof ... Я думаю, что то, что вы хотите, невозможно без стирания тяжелого типа. Этот подход с «вариативной функцией» приведет только к повреждению стека, особенно с std::forward и универсальными ссылками. По сути, каждый раз, когда вы не сопоставляете типы аргументов вызова с типами, ожидаемыми функцией, вашему стеку это совсем не понравится.

Quimby 17.11.2022 20:14

почему бы просто не использовать std::function, чтобы стереть шрифт, или использовать std::any, по крайней мере, для безопасного стирания шрифта?

apple apple 17.11.2022 20:17

@appleapple Потому что std::function по-прежнему требует подписи функции, конечно, не позволяя хранить все методы, которые нельзя легко стереть безопасно.

Quimby 17.11.2022 20:18

@Quimby, это не похоже на то, что OP делает это по-другому

apple apple 17.11.2022 20:19

@appleapple Да, отсюда и мой комментарий о повреждении стека;)

Quimby 17.11.2022 20:20

@Куимби, на всякий случай, мой первый комментарий к OP.

apple apple 17.11.2022 20:22

Я бы попытался использовать указатель на функцию-член вместе с std::invoke. Вы можете получить вдохновение для стертых функций типа из этого ответа SO. Он обращается к void-функциям, но может быть расширен до любого возвращаемого типа.

joergbrech 17.11.2022 20:23

@Quimby просто понимает, что std::function мало помогает, возможно, в тот раз я думал об унификации указателя на член и указатель на функцию-член. (&(static_cast<cat*>(nullptr)->name)) или std::function<void*(T*)> для функции без параметров.

apple apple 17.11.2022 20:30
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
14
97
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы можете создать класс, представляющий любую функцию-член, используя стирание типа (изменено из этого ответа SO). Нет void*, нет многоточия типа C ....

#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;

};

Вы сохраняете std::map<std::string, MemberFunction> в своем class_repr и меняете declare_instance_method и invoke_instance_method следующим образом:

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)...);
}

Живая демонстрация

Обратите внимание, что это прототип. Чтобы сделать это общеприменимым, вам все равно нужно приложить немало усилий: вы должны рассмотреть константные функции-члены и константные аргументы, функции-члены, изменяющие входные данные или возвращающие ссылки и т. д. Также обратите внимание, что std::any сохраняет по значению, поэтому вы можете создать некоторые ненужные копии аргументов функции.

Другие вопросы по теме