Я работаю над утилитой представления классов, которая будет работать аналогично классу 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")
)?
Вы пытаетесь заново реализовать std::invoke?
@TedLyngmo, вы компилируете этот код на С++ 20?
&(static_cast<cat*>(nullptr)->name)
это не очень хорошая идея.
@Quimby этот код не предназначен для переноса, он должен работать под Clang 13, что он и делает.
@RubyNaxela Да, C++20 в g++, clang++ и MSVC. g++ и clang++ выдают похожие сообщения. Однако MSVC не жалуется.
@RubyNaxela Хорошо, вы можете по крайней мере исправить с помощью offsetof
... Я думаю, что то, что вы хотите, невозможно без стирания тяжелого типа. Этот подход с «вариативной функцией» приведет только к повреждению стека, особенно с std::forward
и универсальными ссылками. По сути, каждый раз, когда вы не сопоставляете типы аргументов вызова с типами, ожидаемыми функцией, вашему стеку это совсем не понравится.
почему бы просто не использовать std::function
, чтобы стереть шрифт, или использовать std::any
, по крайней мере, для безопасного стирания шрифта?
@appleapple Потому что std::function
по-прежнему требует подписи функции, конечно, не позволяя хранить все методы, которые нельзя легко стереть безопасно.
@Quimby, это не похоже на то, что OP делает это по-другому
@appleapple Да, отсюда и мой комментарий о повреждении стека;)
@Куимби, на всякий случай, мой первый комментарий к OP.
Я бы попытался использовать указатель на функцию-член вместе с std::invoke. Вы можете получить вдохновение для стертых функций типа из этого ответа SO. Он обращается к void-функциям, но может быть расширен до любого возвращаемого типа.
@Quimby просто понимает, что std::function
мало помогает, возможно, в тот раз я думал об унификации указателя на член и указатель на функцию-член. (&(static_cast<cat*>(nullptr)->name)
) или std::function<void*(T*)>
для функции без параметров.
Вы можете создать класс, представляющий любую функцию-член, используя стирание типа (изменено из этого ответа 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
сохраняет по значению, поэтому вы можете создать некоторые ненужные копии аргументов функции.
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;