跳转到内容

将具有不同签名的函数储存到同一个容器中

Server,有函数成员reg()call(),我想要的效果是,reg()接收一个整数id和一个函数对象,call()可根据id使用传入的参数调用对应的函数对象。就像下面这样:

Server server;
auto handle1{[](const std::string& a, const std::string& b){return a + b;}};
auto handle2{[](int a, double b, float c){return a + b + c;}};
server.reg(1, handle1);
server.reg(2, handle2);
std::cout << server.call(1, "hello", "world") << std::endl;
std::cout << server.call(2, 1, 2, 3) << std::endl;
std::cout << server.call(2, 4, 5, 6) << std::endl;

注:register是关键字,所以用reg代替。

尝试1 粉饰的美好

由“指定的编号”和“值”,通过编号访问值,想到可以用std::unordered_map储存编号和函数。

由于要储存的函数可能是 lambda 函数,可能是普通函数,也可能是函数对象,为了统一,想到使用std::function

std::function是模板,函数的返回值和各参数类型是模板参数,所以不能直接在std::unordered_map里存std::function,想到可以存std::any,在用的时候再用std::any_cast转换为std::function(用传入的参数进行CTAD,类模板参数推导)。

但这样的话,函数的返回值推导不出来,所以妥协一下:作为参数传给register()的函数,返回值统一为int,表示函数调用是否成功;返回值通过非 const 引用参数传递。

最后得到的类Server是这样的:

class Server {
public:
template <typename Handler> void reg(int id, Handler handler) {
m_handlers[id] = std::function(handler);
}
template <typename... Args> int call(int id, Args &&...args) {
if (m_handlers.find(id) != m_handlers.end()) {
try {
auto handler{
std::any_cast<std::function<int(Args...)>>(m_handlers[id])};
int result{std::invoke(handler, std::forward<Args>(args)...)}; // 完美转发
return result;
} catch (const std::exception &e) {
std::cerr << "failure call handler " << id << " " << e.what()
<< std::endl;
return 1;
}
} else {
std::cerr << "handler not found with id " << id << std::endl;
return 1;
}
}
private:
std::unordered_map<int, std::any> m_handlers;
};

测试一下:

Server server;
auto handle1{[](std::string a, std::string b, std::string &r) -> int {
r = a + b;
return 0;
}};
auto handle2{[](int a, double b, double c, double &r) -> int {
r = a + b + c;
return 0;
}};
server.reg(1, handle1);
server.reg(2, handle2);
std::string result;
server.call(1, std::string{"hello"}, std::string{"world"}, result);
std::cout << result << std::endl;
std::string result2;
using namespace std::literals;
server.call(1, "hello"s, "world"s, result2);
std::cout << result2 << std::endl;
double result3;
server.call(2, 1, 2.0, 3.0, result3);
std::cout << result3 << std::endl;
double result4;
server.call(2, 4, 5.0, 6.0, result4);
std::cout << result4 << std::endl;

完整代码:try 01 store functions with different signatures in a container (github.com)

看起来不错嘛,但如果再仔细点去看,可能会对这个 lambda 函数感到奇怪:

auto handle1{ [](std::string a, std::string b, std::string& r) -> int {
r = a + b;
return 0;
} };

为什么前两个参数的类型用std::string而不是用const std::string&std::string_view

以及,这句代码:

server.call(1, std::string{"hello"}, std::string{"world"}, result);

为什么要写成std::string{"hello"}这样呢,直接写"hello"不行吗?

还有这里:

server.call(2, 1, 2.0, 3.0, result3);

为什么要用1, 2.0, 3.0,直接写1, 2, 3不行吗?

确实是不行,确实非得这样写。但实在是有苦衷啊,“非不为也,实不能也”。因为这里:

auto handler{
std::any_cast<std::function<int(Args...)>>(m_handlers[id])};

所调用的函数的签名是根据调用函数时传入的参数来推导的,所以如果我写1, 2, 3而不是1, 2.0, 3.0,那么参数类型就会被推断为int, int, int,而实际上函数的参数类型是int, double, double,这样any_cast()就会失败,就会抛出异常。

如上所述,该方案不可取。

尝试2

[[2024-07-05]] 11:10

上面的方案之所以不可取,是因为在把std::function存入std::any时丢失了函数返回值类型和参数类型这些信息,只能将调用函数时传入的参数(arguments)的类型作为函数参数(parameters)类型。

而之所以要把std::function存入std::any,是因为std::unordered_map的值类型需要是统一的。具有不同模板参数的std::function是不同的类型。

如果有一种数据结构,可以直接存不同类型的值就好了。

std::tuple就是这样一种数据结构。测试如下:

#include <iostream>
#include <string>
#include <tuple>
int main() {
auto handle1{ [](std::string a, std::string b, std::string& r) -> int {
r = a + b;
return 0;
} };
auto handle2{ [](int a, double b, double c, double& r) -> int {
r = a + b + c;
return 0;
} };
std::tuple funcs{ handle1, handle2 };
double r;
std::get<1>(funcs)(1, 2, 3, r);
std::cout << r << std::endl;
return 0;
}

Server当前如下:

template <typename T>
class Server {
public:
Server(T functions) : m_functions{std::move(functions)} {}
template <int num, typename... Args>
void call(Args&&... args) {
std::get<num>(m_functions)(std::forward<Args>(args)...); // 完美转发
}
private:
T m_functions;
};

测试如下:

int main() {
auto handle1{ [](std::string a, std::string b, std::string& r) -> int {
r = a + b;
return 0;
} };
auto handle2{ [](int a, double b, double c, double& r) -> int {
r = a + b + c;
return 0;
} };
std::tuple funcs{ handle1, handle2 };
Server t{ funcs };
double r;
t.call<1>(1, 2, 3, r);
std::cout << r << std::endl;
std::string r2;
t.call<0>("hello", "world", r2);
std::cout << r2 << std::endl;
return 0;
}

跟最开始的设想稍微有些不同。最开始想着可以使用成员函数reg()在运行时动态添加可以调用的函数,现在需要在构造Server实例时传入由std::function组成的std::tuple。最开始想着,调用函数时将函数编号作为函数call()的参数之一传入,现在需要将函数编号作为函数模板call()的模板参数传入。

但想要的效果实现了,不是吗。