首页 > 解决方案 > 什么是在运行时动态转换和调用映射中的变量参数函数而不使用外部库的好方法

问题描述

假设我有多个带有可变参数的函数:

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

这些存储在函数映射中std::strings(函数存储为多态类)。

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

    ...
}

我可以在编译时调用函数,例如:

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::vectorof std::anys 作为参数的函数,然后将函数any_cast设置为它们的类型。虽然这确实有效,但它看起来并不好,它需要所有函数的重复,我宁愿能够编写具有有意义参数的函数而不是模棱两可。


最小示例

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



解决了。

如果您应用接受的解决方案,您可以像我想要的那样从文本中调用函数。这是示例 Tcp 服务器和客户端的新代码。客户端将函数名称和参数作为字符串发送到服务器。然后服务器执行这些。正是我想要的。

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

标签: c++

解决方案


假设您有一个可用于 RPC 的类型列表(以 int 和 string 为例),我们可以将它们组合成一个RPC类型并关联RPCHandler如下:

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

你想创建一个std::map<std::string, RPCHandler> dispatch所以你可以做(​​给定一个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) },
};

wheremake_invoker返回正确形状的 lambda。这个 lambda 的主体将函数指针、参数向量和 astd::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它来查看所有传递的值是否与其被禁止的类型匹配。


推荐阅读