C++无宏实现类成员方法作为回调的模板简化方案
问题背景
现有一组无公共基类的配置类,类中大量方法仅存在两类固定调用签名:
bool fn(const std::string&)bool fn(const std::vector<std::string>&)
需要为所有这类方法的调用封装统一通用逻辑,初始模板封装调用时需要重复书写冗长的类名,冗余度高:
// 初始模板实现(单字符串参数版本) template <typename T, bool(T::*fn)(const std::string&)> bool CFG_STR( T& cfg, const char* key, Nodes data, bool flag ) { /* 内部调用cfg.*fn(x) 实现通用逻辑 */ } // 冗余调用写法,类名需要重复写两次 configClassWithVeryLongName config; success &= CFG_STR< configClassWithVeryLongName, &configClassWithVeryLongName::methodCall >( config, "key", dataStore, configFlag );
要求完全不使用C预处理宏,调用时无需重复书写类名前缀,仅传入方法名或方法名字符串即可完成调用,且不能修改原有类的继承结构。
实现方案
根据使用习惯可以选择两种无宏实现,均不需要修改原有配置类的继承关系。
方案1:C++17 auto非类型模板参数(零运行时开销)
利用C++17支持的auto非类型模板参数,让编译器自动推导成员函数指针对应的类类型,不需要显式重复传入类模板参数,同时可以加静态断言校验方法签名合法性,避免传错方法。
#include <type_traits> // 单std::string参数方法的封装 template <typename T, auto fn> bool wrap_str(T& cfg, const char* key, Nodes data, bool flag) { // 编译期校验签名合法性 static_assert(std::is_invocable_r_v<bool, decltype(fn), T*, const std::string&>, "传入方法必须符合 bool(T::*)(const std::string&) 签名"); // 原有通用逻辑 std::string val = /* 从data按key提取单值的逻辑 */; return flag ? (cfg.*fn)(val) : true; } // std::vector<std::string>参数方法的封装 template <typename T, auto fn> bool wrap_vec(T& cfg, const char* key, Nodes data, bool flag) { static_assert(std::is_invocable_r_v<bool, decltype(fn), T*, const std::vector<std::string>&>, "传入方法必须符合 bool(T::*)(const std::vector<std::string>&) 签名"); // 原有通用逻辑 std::vector<std::string> vals = /* 从data按key提取多值的逻辑 */; return flag ? (cfg.*fn)(vals) : true; }
调用时仅需要在成员指针处写一次类名,不需要重复传类模板参数,编译器会自动从cfg参数推导类类型:
configClassWithVeryLongName config; success &= wrap_str<&configClassWithVeryLongName::methodCall>(config, "key", dataStore, configFlag);
该方案无任何运行时开销,所有校验在编译期完成,代码量最小。
方案2:方法映射表(支持直接传方法名字符串)
如果需要完全省略类名、直接传入方法名字符串完成调用,可以为每个配置类特化一个方法映射表,仅需要在类定义后注册一次方法,后续调用完全不需要写类名前缀。
#include <unordered_map> #include <string_view> // 基础映射表模板,每个配置类需要特化该结构 template <typename T> struct ConfigMethodMap; // 针对长类名的配置类特化映射表,注册所有对应方法 template <> struct ConfigMethodMap<configClassWithVeryLongName> { static inline const std::unordered_map<std::string_view, bool(configClassWithVeryLongName::*)(const std::string&)> str_methods = { {"methodCall", &configClassWithVeryLongName::methodCall}, // 其余单string参数方法在此处注册 }; static inline const std::unordered_map<std::string_view, bool(configClassWithVeryLongName::*)(const std::vector<std::string>&)> vec_methods = { // 其余vector<string>参数方法在此处注册 }; }; // 统一封装接口,直接传方法名字符串 template <typename T> bool wrap_str(T& cfg, std::string_view method_name, const char* key, Nodes data, bool flag) { auto it = ConfigMethodMap<T>::str_methods.find(method_name); if (it == ConfigMethodMap<T>::str_methods.end()) return false; // 方法不存在的错误处理 auto fn = it->second; std::string val = /* 从data按key提取单值的逻辑 */; return flag ? (cfg.*fn)(val) : true; } // 多参数版本封装同理 template <typename T> bool wrap_vec(T& cfg, std::string_view method_name, const char* key, Nodes data, bool flag) { auto it = ConfigMethodMap<T>::vec_methods.find(method_name); if (it == ConfigMethodMap<T>::vec_methods.end()) return false; auto fn = it->second; std::vector<std::string> vals = /* 从data按key提取多值的逻辑 */; return flag ? (cfg.*fn)(vals) : true; }
调用时完全不需要写类名前缀,直接传方法名字符串即可:
configClassWithVeryLongName config; success &= wrap_str(config, "methodCall", "key", dataStore, configFlag);
该方案仅在第一次注册方法时需要写类名,后续所有调用都不需要重复书写,查表的运行时开销极低。
注:C++语法层面不允许在类外不指定类作用域直接获取类成员函数指针,因此如果完全不做注册、也不写类作用域,没有符合标准的实现方式。
内容的提问来源于stack exchange,提问作者rand'Chris
相关产品推荐
相关产品推荐

