Каков хороший способ динамического приведения и вызова функций с переменными аргументами из map во время выполнения БЕЗ использования внешней библиотеки

#c

#c

Вопрос:

Допустим, я использую несколько функций с переменными аргументами:

 void greetWorld() {
    cout << "Hello World!" << endl;
}

void greetName(const stringamp; name) {
    cout << "Hello " << name << "!" << endl;
}

void printAddition(const int lhs, const int rhs) {
    cout << "Addition: " << to_string(lhs   rhs) << endl;
}
  

И они хранятся в map std::strings функций to (функции хранятся как полиморфный класс).

 template<typename... Args>
class DerivedFunction;

class BaseFunction {
public:
    template<typename... Args>
    void operator()(Args... args) const {
        (*static_cast<const DerivedFunction<Args...>*>(this))(args...);
    }
};

template<typename... Args>
class DerivedFunction : public BaseFunction {
public:
    DerivedFunction(void(*function)(Args...)) {
        this->function = function;
    }
    void operator()(Args... args) const {
        function(args...);
    }
private:
    void(*function)(Args...);
};

template<typename... Args>
unique_ptr<DerivedFunction<Args...>> make_function(
    void(*function)(Args...)
) {
    return std::make_unique<DerivedFunction<Args...>>(function);
}

int main() {
    unordered_map<string, unique_ptr<BaseFunction>> function_map;
    function_map.insert({ "greetWorld",    make_function(amp;greetWorld)    });
    function_map.insert({ "greetName",     make_function(amp;greetName)     });
    function_map.insert({ "printAddition", make_function(amp;printAddition) });

    ...
}
  

Я могу вызывать функции во время компиляции, например:

 int main() {
    ...

    (*function_map.at("greetWorld"))();
    (*function_map.at("greetName"))("Foo"s);
    (*function_map.at("printAddition"))(1, 2);
}
  

Если у меня тогда есть строка или поток, подобный:

 greetWorld
greetName     string Foo
printAddition int    1   int 2
  

Каков был бы хороший способ вызова функций?
Я не могу найти какой-либо способ приведения типа во время выполнения.


Почему?

Я пытаюсь реализовать какую-то процедуру удаленного вызова в учебных целях. Я не хочу использовать внешнюю библиотеку, поскольку я пытаюсь узнать, как реализовать это с помощью стандартной библиотеки C , чтобы лучше понять C .


Что я пробовал?

Не так много. Я тестировал создание функций, которые принимают a std::vector из std::any s в качестве аргумента, а затем any_cast присваивали им тот тип, которым они являются. Хотя это работает, это выглядит не очень красиво, для этого требуются дубликаты всех функций, я бы предпочел иметь возможность писать функции со значимыми аргументами, чем неоднозначные.


Minimum Example

 #include <iostream>
#include <string>
#include <unordered_map>
#include <memory>
using namespace std;

void greetWorld() {
    cout << "Hello World!" << endl;
}

void greetName(const stringamp; name) {
    cout << "Hello " << name << "!" << endl;
}

void printAddition(const int lhs, const int rhs) {
    cout << "Addition: " << to_string(lhs   rhs) << endl;
}

template<typename... Args>
class DerivedFunction;

class BaseFunction {
public:
    template<typename... Args>
    void operator()(Args... args) const {
        (*static_cast<const DerivedFunction<Args...>*>(this))(args...);
    }
};

template<typename... Args>
class DerivedFunction : public BaseFunction {
public:
    DerivedFunction(void(*function)(Args...)) {
        this->function = function;
    }
    void operator()(Args... args) const {
        function(args...);
    }
private:
    void(*function)(Args...);
};

template<typename... Args>
unique_ptr<DerivedFunction<Args...>> make_function(
    void(*function)(Args...)
) {
    return std::make_unique<DerivedFunction<Args...>>(function);
}

int main() {
    unordered_map<string, unique_ptr<BaseFunction>> function_map;
    function_map.insert({ "greetWorld",    make_function(amp;greetWorld)    });
    function_map.insert({ "greetName",     make_function(amp;greetName)     });
    function_map.insert({ "printAddition", make_function(amp;printAddition) });

    cout << "Calling functions at compile time." << endl << endl;

    (*function_map.at("greetWorld"))();
    (*function_map.at("greetName"))("Foo"s);
    (*function_map.at("printAddition"))(1, 2);

    //cout << endl << "Calling functions at runtime." << endl << endl;
    //string runtime =
    //  "greetWorldn"
    //  "greetName     string Foon"
    //  "printAddition int    1   int 2";
    //
    // todo: call functions
}
  



Решаемая.

Если вы примените принятое решение, вы можете вызывать функции из текста, как я и хотел. Вот новый код для примера Tcp-сервера и клиента. Клиент отправляет имена функций и аргументы в виде строки на сервер. Затем сервер выполняет их. Именно то, что я хотел.

 struct FunctionNameAndArguments {
    string function_name;
    vector<RPC> arguments;
};

FunctionNameAndArguments parseFunctionNameAndArguments(
    const stringamp; function_name_and_arguments_string
) {
    istringstream ss(function_name_and_arguments_string);
    FunctionNameAndArguments function_name_and_arguments;
    // function name
    ss >> function_name_and_arguments.function_name;
    // arguments
    autoamp; arguments = function_name_and_arguments.arguments;
    while (!ss.eof()) {
        string function_type;
        ss >> function_type;
        // integer
        if (function_type == "int") {
            int value;
            ss >> value;
            arguments.push_back(value);
        }
        // string
        else if (function_type == "string") {
            string value;
            ss >> value;
            arguments.push_back(value);
        }
        else {
            throw exception("unknown argument type");
        }
    }
    return function_name_and_arguments;
}

int main() {
    unordered_map<string, RPCHandler> functions = {
        { "greetWorld", make_invoker(amp;greetWorld) },
        { "greetName", make_invoker(amp;greetName) },
        { "printAddition", make_invoker(amp;printAddition) }
    };

    char server;
    cout << "Server? (y/n): " << endl;
    cin >> server;
    // server
    if (server == 'y') {
        // accept client
        TcpListener listen;
        listen.listen(25565);
        TcpSocket client;
        listen.accept(client);

        size_t received;
        // receive size of string
        size_t size;
        client.receive(amp;size, sizeof(size), received);
        // receive function name and arguments as string
        string function_name_and_arguments_string;
        function_name_and_arguments_string.resize(size);
        client.receive(
            function_name_and_arguments_string.data(),
            size,
            received
        );
        // go through each line
        istringstream lines(function_name_and_arguments_string);
        string line;
        while (getline(lines, line)) {
            // parse function name and arguments
            auto [function_name, arguments] = parseFunctionNameAndArguments(
                line
            );
            // call function
            functions.at(function_name)(
                arguments
            );
        }
        
    }
    // client
    else {
        // connect to server
        TcpSocket server;
        server.connect("localhost", 25565);

        // function calls string
        const string function_calls =
            "greetWorldn"
            "greetName     string Foon"
            "printAddition int    1   int 2";
        size_t size = function_calls.size();
        // send size of string
        server.send(amp;size, sizeof(size));
        // send function calls string
        server.send(function_calls.data(), size);
    }
}
  

Комментарии:

1. any way to cast a type at runtime так, например, просто reinterpret_cast ? Извините, в C нет отражения . Если вы этого хотите, вы должны реализовать это самостоятельно.

2. процедура удаленного вызова — для этого вам необходимо сохранить ваши функции вместе с их аргументами. Сохраняйте лямбда-выражение с захватом в std::function<void()> . Ваш удаленный исполнитель вызывает их std::function<void()> .

3. @KamilCuk, похоже, это требует большого изменения функций, тогда как библиотеки, подобные rpclib этим, могут делать это вообще без изменения функции ( srv.bind("printAddition", amp;printAddition); это все, что мне нужно сделать, чтобы связать ее, и client.call("printAddition", 1, 2); все, что мне нужно сделать, чтобы вызвать ее). Я понимаю, что библиотека с открытым исходным кодом, поэтому я могу видеть, как они это делают, но для меня это слишком сложно понять

4. @MaximEgorushkin Я попробую то, что вы сказали

5. Вы сопоставляете строку времени выполнения с типами, имея таблицу поиска. Это означает, что вы можете поддерживать только фиксированный набор типов.

Ответ №1:

Предположим, у вас есть список типов (например, int и string), которые можно использовать в RPC, мы можем объединить их в RPC тип и связать RPCHandler следующим образом:

 using RPC = std::variant<int, std::string>;
using RPCHandler = std::function<void(std::vector<RPC>)>;
  

Вы хотите создать std::map<std::string, RPCHandler> dispatch so, чтобы вы могли сделать (учитывая a std::vector<RPC> args ):

 dispatch[command](args);
  

Эта карта может быть построена следующим образом:

 void test0();
void test2(int, std::string);

std::map<std::string, RPCHandler> dispatch = {
  { "test0", make_invoker(test0) },
  { "test2", make_invoker(test2) },
};
  

где make_invoker возвращает лямбда-выражение правильной формы.
Тело этого лямбда-выражения передает указатель на функцию, вектор аргументов и a std::index_sequence в invoke_rpc :

 template<class... Arg>
RPCHandler make_invoker(void (*f)(Arg...)) {
   return [f](std::vector<RPC> args) {
      invoke_rpc(f, args, std::index_sequence_for <Arg...>{});
   };
}
  

Наконец, invoke_rpc использует std::get каждый аргумент по очереди, чтобы преобразовать его в ожидаемый тип. Он делает это путем параллельного расширения двух заданных пакетов параметров шаблона. Интуитивно понятно, что это расширяется до f(std::get<Arg0>(args.at(0), std::get<Arg1>(args.at(1)) такого количества аргументов f , сколько он ожидает (поскольку последовательность индексов имеет одинаковую длину Args... ).

 template<class... Arg, std::size_t... I>
void invoke_rpc(void (*f)(Arg...), std::vector<RPC> args, std::index_sequence<I...>) {
    f(std::get<Arg>(args.at(I))...);
}
  

Если вектор слишком короткий, вы получаете std::out_of_range ошибку, если есть несоответствие аргументов, вы получаете std::bad_variant_access . Вы можете улучшить обработку ошибок, проверив размер args перед вызовом f и используя std::holds_alternative , чтобы увидеть, соответствуют ли все переданные значения их запрещенному типу.

Комментарии:

1. Потрясающе! Это работает со всеми моими примерами функций (хотя greetName аргумент ‘s пришлось изменить с const stringamp; name на const string name ). Это очень чистый синтаксис и визуально.

2. Он использует несколько функций, которые я в настоящее время не знаю, мне нужно их исследовать. Сверху вниз: я никогда не слышал о std::index_sequence nor std::index_sequence_for . Я никогда раньше не видел два пакета параметров в одной шаблонной функции. Я узнал о пакетах параметров только несколько недель назад. Я не думал, что это будет работать с двумя пакетами параметров (потому что как он узнает, когда заканчивается один и начинается другой?). Я также не знал, что пакеты параметров могут использовать определенный тип (например std::size_t ). Я также никогда не видел ... not непосредственно рядом с шаблонным параметром : (args.at(I))... .

3. Я полагаю, что вы можете использовать std::remove_cv и std::remove_reference вместе в параметре шаблона std::get , чтобы постоянство или ссылочность аргументов функции не конфликтовали с вашим вариантом.

4. Что касается ресурсов, я смутно помню, как читал это и объединял его со страницами cppreference для расширения пакета параметров шаблона.

5. f(EXPR...) расширяется EXPR для пакетов параметров шаблона, на которые ссылаются ссылки, и превращает их в аргументы f . Предположительно, вы можете сделать это без index_sequence , но эта форма близка к тому, как я ее реализовал три года назад.