如何实现模板化AcceptFunctions消息处理接口及constexpr函数映射?
现有一套消息处理代码,消息处理接口与消息定义相互独立,代码如下:
现有代码文件
msg_io.h
/** * @file msg_io.h */ struct MsgIO { virtual ~MsgIO() {} };
handler.h
/** * @file handler.h */ #include <map> #include "msg_io.h" template<class _msgHandler> struct HandlerInterface { using HandlerFunc = bool (int, const MsgIO& ); using HandlerFuncPtr = HandlerFunc _msgHandler::*; template<class T> using AcceptFunc = bool(int, const T*); template<class T> using AcceptFuncPtr = AcceptFunc<T> _msgHandler::*; using HandlerMap = std::map<unsigned, HandlerFuncPtr>; };
server.h
/** * @file server.h */ #include "handler.h" struct ConcreteHandler : HandlerInterface<ConcreteHandler> { enum Messages { MSG_1, MSG_2, MSG_3 }; // 疑问:这样的声明合规吗?能否放到模板里? AcceptFunc<struct Msg1> accept; AcceptFunc<struct Msg2> accept; AcceptFunc<struct Msg3> accept; template<class T> bool acceptMsg(int ind, const MsgIO& m) { auto mp = dynamic_cast<const T*>(&m); return mp ? accept(ind, mp) : false; } static HandlerMap handlers; template<class T> static const constexpr HandlerFuncPtr Handle = &ConcreteHandler::acceptMsg<T>; };
messages.h
/** * @file messages.h */ #include "msg_io.h" struct Msg1: MsgIO {}; struct Msg2: MsgIO {}; struct Msg3: MsgIO {};
handlermap.cpp
/** * @file handlermap.cpp */ #include "server.h" #include "messages.h" // 疑问:能否改成constexpr? ConcreteHandler::HandlerMap ConcreteHandler::handlers{ { MSG_1, Handle<Msg1> }, { MSG_2, Handle<Msg2> }, { MSG_3, Handle<Msg3> }, { MSG_10, Handle<Msg1> } };
handler.cpp
/** * @file handler.cpp */ #include "server.h" #include "messages.h" bool ConcreteHandler::accept(int, const Msg1*) { return true; } bool ConcreteHandler::accept(int, const Msg2*) { return true; } bool ConcreteHandler::accept(int, const Msg3*) { return true; }
当前可通过ConcreteHandler::handlers.find()根据消息编号获取对应的acceptMsg函数。现需完成以下改造:
- 将实现重构为形如
AcceptFunctions<Msg1, Msg2, Msg3>::accept的模板化接口,同时配套对应的AcceptFunctions<Msg1, Msg2, Msg3>::handlers; - 确认是否可以将函数映射表以
constexpr方式在编译期完成初始化。
1. 模板化接口改造:AcceptFunctions可变参数模板
我们可以用可变参数模板实现通用的AcceptFunctions,自动适配任意数量的消息类型,同时保留原有的消息处理逻辑:
改造后的核心代码
通用handler基模板
#include <map> #include <type_traits> #include "msg_io.h" // 前置声明 template<typename... Msgs> struct AcceptFunctions; // 递归终止:无消息类型的情况 template<> struct AcceptFunctions<> {}; // 可变参数模板实现 template<typename FirstMsg, typename... RestMsgs> struct AcceptFunctions<FirstMsg, RestMsgs...> : AcceptFunctions<RestMsgs...> { // 继承基类的accept重载,实现多消息类型的accept using AcceptFunctions<RestMsgs...>::accept; // 当前消息类型的accept函数签名 using AcceptFunc = bool(int, const FirstMsg*); AcceptFunc accept; // 通用的消息转发函数,和原acceptMsg逻辑一致 template<typename T> bool acceptMsg(int ind, const MsgIO& m) { if constexpr (std::is_base_of_v<MsgIO, T>) { if (const auto* mp = dynamic_cast<const T*>(&m)) { return accept(ind, mp); } } return false; } // 消息处理函数指针类型 using HandlerFunc = bool(int, const MsgIO&); using HandlerFuncPtr = HandlerFunc AcceptFunctions::*; // 模板化的Handle指针,指向当前类型的acceptMsg template<typename T> static constexpr HandlerFuncPtr Handle = &AcceptFunctions::acceptMsg<T>; // 消息映射表类型 using HandlerMap = std::map<unsigned, HandlerFuncPtr>; static HandlerMap handlers; }; // 初始化映射表的辅助函数 template<typename... Msgs> typename AcceptFunctions<Msgs...>::HandlerMap init_handlers(const std::initializer_list<std::pair<unsigned, typename AcceptFunctions<Msgs...>::HandlerFuncPtr>>& init) { return typename AcceptFunctions<Msgs...>::HandlerMap(init); }
使用示例
// 定义具体的消息处理类型 using MyMsgHandler = AcceptFunctions<Msg1, Msg2, Msg3>; // 实现各个消息的accept函数 bool MyMsgHandler::accept(int ind, const Msg1*) { /* 处理Msg1逻辑 */ return true; } bool MyMsgHandler::accept(int ind, const Msg2*) { /* 处理Msg2逻辑 */ return true; } bool MyMsgHandler::accept(int ind, const Msg3*) { /* 处理Msg3逻辑 */ return true; } // 初始化映射表 template<> MyMsgHandler::HandlerMap MyMsgHandler::handlers = init_handlers<Msg1, Msg2, Msg3>({ { 1, MyMsgHandler::Handle<Msg1> }, { 2, MyMsgHandler::Handle<Msg2> }, { 3, MyMsgHandler::Handle<Msg3> }, { 10, MyMsgHandler::Handle<Msg1> } });
原代码中accept重载的合规性说明:原ConcreteHandler中重复声明accept是合规的——这是C++的函数重载,只要每个accept的参数列表不同(这里是const Msg1*/const Msg2*等不同类型),编译器就能正确区分,且这种写法可以放到模板中(如上面的AcceptFunctions模板所示)。
2. 映射表的constexpr编译期初始化
限制说明
标准库的std::map在C20及之前都不支持constexpr构造(C23开始部分支持,但仍受限于动态内存分配的constexpr支持),因此无法直接用constexpr初始化std::map类型的handlers。
替代方案:编译期构建静态数组,运行时转为map/直接查找
我们可以用constexpr std::array存储键值对,在编译期完成初始化,运行时再将其转为std::map,或者直接用数组做线性查找(如果消息编号不多,效率足够):
constexpr数组实现
#include <array> #include <utility> // 定义constexpr键值对类型 template<typename HandlerPtr> using ConstexprHandlerEntry = std::pair<unsigned, HandlerPtr>; // 在AcceptFunctions模板中添加constexpr数组 template<typename FirstMsg, typename... RestMsgs> struct AcceptFunctions<FirstMsg, RestMsgs...> : AcceptFunctions<RestMsgs...> { // ... 保留之前的代码 ... // 编译期初始化的键值对数组 template<ConstexprHandlerEntry<HandlerFuncPtr>... Entries> static constexpr auto make_constexpr_handlers() { return std::array<ConstexprHandlerEntry<HandlerFuncPtr>, sizeof...(Entries)>{Entries...}; } // 示例:编译期初始化的静态数组 static constexpr auto constexpr_handlers = make_constexpr_handlers< {1, Handle<Msg1>}, {2, Handle<Msg2>}, {3, Handle<Msg3>}, {10, Handle<Msg1>} >(); }; // 运行时将constexpr数组转为std::map template<> MyMsgHandler::HandlerMap MyMsgHandler::handlers = [](){ HandlerMap map; for (const auto& entry : MyMsgHandler::constexpr_handlers) { map.insert(entry); } return map; }();
这种方式下,constexpr_handlers是完全在编译期初始化的,运行时只是将其拷贝到std::map中,既满足了编译期构建的需求,又保留了std::map的高效查找特性。
如果消息数量较少,也可以直接在编译期通过constexpr函数实现查找逻辑,完全避免运行时的map开销:
// 编译期查找函数 template<typename... Msgs> constexpr typename AcceptFunctions<Msgs...>::HandlerFuncPtr find_handler(unsigned msg_id) { for (const auto& entry : AcceptFunctions<Msgs...>::constexpr_handlers) { if (entry.first == msg_id) { return entry.second; } } return nullptr; } // 使用示例(编译期调用) constexpr auto handler_for_msg1 = find_handler<Msg1, Msg2, Msg3>(1);
内容的提问来源于stack exchange,提问作者Swift - Friday Pie

