C++如何向模板类构造函数传入可调用对象并支持后续调用
实现方案
核心采用固定签名的轻量类型擦除设计,既不需要给MyClass增加额外模板参数,也不要求可调用对象继承特定基类,同时支持引用持有外部可调用对象,保证有状态仿函数的状态跨实例共享。
核心逻辑
- 在
MyClass<T>内部定义抽象可调用基类,仅约定invoke(T) -> float的调用接口和克隆接口 - 针对任意传入的可调用类型,派生模板化的实现类,内部仅持有原可调用对象的引用,不做拷贝
MyClass内部通过基类指针持有擦除类型后的可调用包装,构造时根据传入的可调用类型自动实例化对应的实现类- 正确实现拷贝/移动构造和赋值函数,满足
std::variant和std::vector对元素可拷贝/移动的要求
#include <variant> #include <vector> #include <functional> #include <iostream> #include <utility> template <typename T> class MyClass { // 类型擦除抽象基类 struct CallableBase { virtual float invoke(T arg) = 0; virtual CallableBase* clone() const = 0; virtual ~CallableBase() = default; }; // 具体可调用类型的包装实现,仅持有原对象引用 template <typename F> struct CallableImpl final : CallableBase { F& func_ref; explicit CallableImpl(F& f) : func_ref(f) {} float invoke(T arg) override { return func_ref(std::move(arg)); } CallableBase* clone() const override { return new CallableImpl<F>(func_ref); } }; T a; CallableBase* f; public: // 构造函数自动推导传入的可调用类型 template <typename Callable> MyClass(T _a, Callable* _f) : a(std::move(_a)), f(new CallableImpl<Callable>(*_f)) {} ~MyClass() { delete f; } // 实现深拷贝,避免浅拷贝导致的重复释放 MyClass(const MyClass& other) : a(other.a), f(other.f->clone()) {} MyClass& operator=(const MyClass& other) { if (this != &other) { delete f; a = other.a; f = other.f->clone(); } return *this; } MyClass(MyClass&& other) noexcept : a(std::move(other.a)), f(other.f) { other.f = nullptr; } MyClass& operator=(MyClass&& other) noexcept { if (this != &other) { delete f; a = std::move(other.a); f = other.f; other.f = nullptr; } return *this; } float getFloat() { return f->invoke(a); } };
关键注意事项
- 之前出现空指针调用问题,核心原因通常是两点:一是类型擦除层未正确初始化基类指针,二是传入临时可调用对象的地址产生悬垂引用;使用时需保证传入的可调用对象生命周期长于持有它的
MyClass实例 - 传入lambda时需要先将lambda实例化为具名变量再取地址传入,C++不允许直接取临时lambda对象的地址,同时该写法也能保证lambda生命周期符合要求
- 实现中仅持有可调用对象的引用,不会拷贝原对象,因此有状态仿函数的计数会在所有指向它的
MyClass实例间共享,完全符合预期逻辑 MyClass仅保留T作为模板参数,因此std::variant<MyClass<int>, MyClass<float>, MyClass<bool>>的定义完全合法,可直接存入vector统一遍历
测试验证
以下测试代码完全匹配需求中的调用逻辑:
// 测试用可调用对象定义 static float minus1(int num) { return num - 1; } class Stateful { public: Stateful() : num_calls(0) {} float operator()(int num) { ++num_calls; return static_cast<float>(num) - (static_cast<float>(num_calls) * 0.5f); } private: int num_calls; }; static std::function<float(float)> invert = [](float a){ return -a; }; int main() { using Element = std::variant<MyClass<int>, MyClass<float>, MyClass<bool>>; std::vector<Element> vec; MyClass<int> option1(-5, &minus1); MyClass<int> option1a(99, &minus1); std::cout << option1.getFloat() << std::endl; // 输出-6 std::cout << option1a.getFloat() << std::endl; // 输出98 static Stateful stateful{}; MyClass<int> option2(10, &stateful); std::cout << option2.getFloat() << std::endl; // 输出9.5 std::cout << option2.getFloat() << std::endl; // 输出9 MyClass<int> option2a(100, &stateful); std::cout << option2a.getFloat() << std::endl; // 输出98.5 std::cout << option2.getFloat() << std::endl; // 输出8 MyClass<float> option3(1.602f, &invert); MyClass<float> option3a(-6.022f, &invert); std::cout << option3a.getFloat() << std::endl; // 输出6.022 std::cout << option3.getFloat() << std::endl; // 输出-1.602 float pi = 3.14f; // lambda先实例化为具名变量再传地址 auto pi_lambda = [&pi](bool b){return (b ? -pi : 0.f);}; MyClass<bool> option4(true, &pi_lambda); std::cout << option4.getFloat() << std::endl; // 输出-3.14 // 放入vector遍历测试 vec.push_back(option1); vec.push_back(option2); vec.push_back(option2a); vec.push_back(option3); vec.push_back(option3a); vec.push_back(option4); for (auto& option : vec) { std::visit([](auto&& arg) { std::cout << arg.getFloat() << std::endl; }, option); } return 0; }
内容的提问来源于stack exchange,提问作者rbv
相关产品推荐
相关产品推荐

