如何为带成员函数指针模板参数的Callee类实现nullptr特化?
现有代码与场景
我们需要实现一个将成员函数指针作为模板参数的Callee类,用于通过统一接口调用目标对象的成员函数。现有Callee类定义如下:
template <typename T, T> class Callee; // 针对成员函数指针的特化 template <typename TObject, typename... TParams, typename TReturn, TReturn (TObject::*Function)(TParams...)> class Callee<TReturn (TObject::*)(TParams...), Function> { public: Callee(TObject& instance) : instance_{instance} { } TReturn call(TParams... data) { return (instance_.*Function)(std::forward<TParams>(data)...); } private: TObject& instance_; };
配套的CalleeCreator类用于创建Callee实例:
// 前向声明 template <typename T> class CalleeCreator; // 主模板 template <typename T> class CalleeCreator { }; // 针对函数签名的特化 template <typename TReturn, typename... TParams> class CalleeCreator<TReturn(TParams...)> { template <typename TObject, TReturn (TObject::*Function)(TParams...)> static Callee<TReturn (TObject::*)(TParams...), Function>* createCallee(TObject& instance) { return new Callee<TReturn (TObject::*)(TParams...), Function>(instance); } };
实例化方式示例:
class FuncClass { public: uint32_t func(uint32_t); }; FuncClass funcClass; // 创建调用FuncClass::func的Callee实例 auto callee = CalleeCreator<uint32_t(uint32_t)>::createCallee<FuncClass, &FuncClass::func>(funcClass);
现在需要实现一个哑元Callee,用于替代空指针场景,调用时无实际逻辑,仅返回默认构造的返回值。尝试的特化代码如下:
template <typename TObject, typename TReturn, typename... TParams> class Callee<TReturn (TObject::*)(TParams...), nullptr> { public: Callee() = default; TReturn call(TParams... data) { return TReturn{}; } };
但编译时出现错误:
error: non-type template argument specializes a template parameter with dependent type 'T' class Callee<TReturn (TObject::*)(TParams...), nullptr> note: template parameter is declared here template <typename T, T> class Callee; ~^
原因分析
C++标准规定:当非类型模板参数的类型是依赖类型(即依赖于模板参数的类型,这里T是TReturn (TObject::*)(TParams...),依赖于TObject、TReturn等模板参数)时,不能直接用nullptr作为实参进行特化。编译器无法在特化匹配阶段确定nullptr是否符合依赖类型T的要求。
可行解决方案
方案1:新增独立的哑元类,通过统一接口适配
无需修改原有Callee模板,新增一个实现相同接口的CalleeDummy类,用于哑元场景:
首先确保所有Callee类都继承自同一个纯虚接口(假设接口为ICallee):
// 统一接口类 template <typename Sig> class ICallee; template <typename TReturn, typename... TParams> class ICallee<TReturn(TParams...)> { public: virtual ~ICallee() = default; virtual TReturn call(TParams... data) = 0; };
修改原有Callee类继承自该接口:
template <typename TObject, typename... TParams, typename TReturn, TReturn (TObject::*Function)(TParams...)> class Callee<TReturn (TObject::*)(TParams...), Function> : public ICallee<TReturn(TParams...)> { // 原有实现不变 };
然后实现哑元类:
template <typename Sig> class CalleeDummy; template <typename TReturn, typename... TParams> class CalleeDummy<TReturn(TParams...)> : public ICallee<TReturn(TParams...)> { public: TReturn call(TParams... /*data*/) override { return TReturn{}; } };
最后在CalleeCreator中添加创建哑元实例的方法:
template <typename TReturn, typename... TParams> class CalleeCreator<TReturn(TParams...)> { // 原有createCallee方法不变 static ICallee<TReturn(TParams...)>* createDummyCallee() { return new CalleeDummy<TReturn(TParams...)>(); } };
使用时直接调用createDummyCallee即可获得哑元实例,完全适配原有接口。
方案2:调整模板参数,支持nullptr特化
如果坚持要通过Callee的特化实现,可以修改主模板的参数设计,将非类型参数的类型从依赖类型改为固定的std::nullptr_t或使用auto(C++17及以上):
方式A:修改主模板为可选参数
#include <cstddef> // 引入std::nullptr_t // 调整主模板,第二个参数默认值为nullptr,类型为std::nullptr_t或T template <typename T, typename U = std::nullptr_t, U Func = nullptr> class Callee; // 原有正常成员函数指针的特化 template <typename TObject, typename... TParams, typename TReturn, TReturn (TObject::*Function)(TParams...)> class Callee<TReturn (TObject::*)(TParams...), decltype(Function), Function> : public ICallee<TReturn(TParams...)> { // 原有实现不变 }; // 哑元版本特化 template <typename TObject, typename TReturn, typename... TParams> class Callee<TReturn (TObject::*)(TParams...), std::nullptr_t, nullptr> : public ICallee<TReturn(TParams...)> { public: Callee() = default; TReturn call(TParams... /*data*/) override { return TReturn{}; } };
实例化哑元时:
auto dummy = new Callee<decltype(&FuncClass::func)>();
方式B:使用C++17的auto非类型模板参数
如果项目支持C++17,可以简化模板设计:
template <auto Func = nullptr> class Callee; // 正常成员函数指针特化 template <typename TObject, typename... TParams, typename TReturn, TReturn (TObject::*Function)(TParams...)> class Callee<Function> : public ICallee<TReturn(TParams...)> { public: Callee(TObject& instance) : instance_{instance} {} TReturn call(TParams... data) override { return (instance_.*Function)(std::forward<TParams>(data)...); } private: TObject& instance_; }; // nullptr特化 template <> template <typename TReturn, typename... TParams> class Callee<nullptr> : public ICallee<TReturn(TParams...)> { public: TReturn call(TParams... /*data*/) override { return TReturn{}; } };
这种方式下,哑元实例化更简洁:
auto dummy = new Callee<nullptr>();
总结
方案1无需修改原有模板结构,通过新增类实现接口适配,兼容性最好;方案2需要调整模板参数设计,适合允许修改原有模板的场景。根据项目实际情况选择即可。
内容的提问来源于stack exchange,提问作者NetoBF

