#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
norstd::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 , но эта форма близка к тому, как я ее реализовал три года назад.