Let's say I multiple functions with variable arguments:
void greetWorld() {
cout << "Hello World!" << endl;
}
void greetName(const string& name) {
cout << "Hello " << name << "!" << endl;
}
void printAddition(const int lhs, const int rhs) {
cout << "Addition: " << to_string(lhs + rhs) << endl;
}
And these are stored in a map of std::strings
to functions (functions being stored as a polymorphic class).
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(&greetWorld) });
function_map.insert({ "greetName", make_function(&greetName) });
function_map.insert({ "printAddition", make_function(&printAddition) });
...
}
I can call the functions at compile time like:
int main() {
...
(*function_map.at("greetWorld"))();
(*function_map.at("greetName"))("Foo"s);
(*function_map.at("printAddition"))(1, 2);
}
If I then have a string, or stream like:
greetWorld
greetName string Foo
printAddition int 1 int 2
What would be a good way to call the functions? I can not figure out any way to cast a type at runtime.
I am trying to implement some kind of remote call procedure for learning purposes. I do not want to use an external library as I am trying to learn how to implement this with the C++ standard library for the sake of understanding C++ more.
Not much. I've tested creating functions that take a std::vector
of std::any
s as an argument, and then had the function any_cast
them to the type they are. Whilst this does work, it does not look nice, it requires duplicates of all functions, I would rather be able to write functions with meaningful arguments than ambigious.
#include <iostream>
#include <string>
#include <unordered_map>
#include <memory>
using namespace std;
void greetWorld() {
cout << "Hello World!" << endl;
}
void greetName(const string& 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(&greetWorld) });
function_map.insert({ "greetName", make_function(&greetName) });
function_map.insert({ "printAddition", make_function(&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 =
// "greetWorld\n"
// "greetName string Foo\n"
// "printAddition int 1 int 2";
//
// todo: call functions
}
If you apply the accepted solution, you can call functions from the text like I had wanted. Here is new code for an example Tcp server and client. The client sends function names and arguments as a string to the server. The server then executes these. Exactly what I wanted.
struct FunctionNameAndArguments {
string function_name;
vector<RPC> arguments;
};
FunctionNameAndArguments parseFunctionNameAndArguments(
const string& 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
auto& 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(&greetWorld) },
{ "greetName", make_invoker(&greetName) },
{ "printAddition", make_invoker(&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(&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 =
"greetWorld\n"
"greetName string Foo\n"
"printAddition int 1 int 2";
size_t size = function_calls.size();
// send size of string
server.send(&size, sizeof(size));
// send function calls string
server.send(function_calls.data(), size);
}
}
Let us assume you have a list of types (taking int and string as an example) usable in RPC, we can combine them in a RPC
type and associated RPCHandler
as follows:
using RPC = std::variant<int, std::string>;
using RPCHandler = std::function<void(std::vector<RPC>)>;
You want to create a std::map<std::string, RPCHandler> dispatch
so you can do (given a std::vector<RPC> args
):
dispatch[command](args);
This map can be constructed as follows:
void test0();
void test2(int, std::string);
std::map<std::string, RPCHandler> dispatch = {
{ "test0", make_invoker(test0) },
{ "test2", make_invoker(test2) },
};
where make_invoker
returns a lambda of the correct shape.
The body of this lambda passes the function pointer, argument vector, and a std::index_sequence
to 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...>{});
};
}
Finally, invoke_rpc
uses std::get
on each argument in turn to convert it into the expected type. It does this by expanding the two given template parameter packs in parallel. Intuitively, this expands to f(std::get<Arg0>(args.at(0), std::get<Arg1>(args.at(1))
with as many arguments to f
as it expects (since the index sequence has the same length 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))...);
}
If the vector is too short you get a std::out_of_range
error, if there is an argument mismatch you get a std::bad_variant_access
. You can improve error handling by checking the size of args
before calling f
, and using std::holds_alternative
to see if all passed values match their proscribed type.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With