如何设计支持比较操作的C++回调?实现列表中查找与移除
注意:此问题并非询问“如何比较std::function对象的相等性”(通常结论是无法直接实现),核心需求是如何定义一种可同时容纳静态函数、成员函数以及带捕获的lambda的回调类型,使其能存入容器并支持相等比较,用于回调注册时去重和移除操作。
场景背景
最初使用简单的函数指针作为回调类型:
typedef bool(*Callback)(const MyClass&);
随着设计推进,需要支持成员函数作为回调,因此改用带捕获的lambda:
auto myLambda = [this](const MyClass& myclass) {...};
后续切换到std::function来统一回调类型:
typedef std::function<bool(const MyClass&)> Callback;
但将Callback存入std::vector后,尝试的查找代码完全无效,所有比较都会返回匹配:
bool findCallback(const Callback& cb, const std::vector<Callback>& callback_v) { std::find_if(callback_v.begin(), callback_v.end(),[cb](Callback& other) { return *cb.target<bool(const MyClass&)>()==*other.target<bool(const MyClass&)>(); }) != callback_v.end(); }
原因是std::function::target()仅对存储的原始类型(如函数指针)有效,对于lambda、绑定的成员函数等会返回nullptr,解引用会导致未定义行为,自然无法区分不同的回调对象。
解决方案
方案1:自定义回调包装类,内置相等比较逻辑
自己实现一个包装类,统一处理不同类型的可调用对象,并为每种类型实现对应的相等判断逻辑:
#include <functional> #include <typeindex> #include <typeinfo> #include <memory> class Callback { public: // 包装静态函数/函数指针 Callback(bool(*func)(const MyClass&)) : m_impl(new FuncImpl(func)) {} // 包装成员函数(绑定对象指针) template <typename T> Callback(T* obj, bool(T::*mem_func)(const MyClass&)) : m_impl(new MemFuncImpl<T>(obj, mem_func)) {} // 包装带捕获的lambda(要求lambda可复制且支持==比较) template <typename F> Callback(F func, typename std::enable_if_t<!std::is_same_v<F, Callback>>* = nullptr) : m_impl(new LambdaImpl<F>(std::move(func))) {} // 相等比较运算符 bool operator==(const Callback& other) const { if (m_impl->type() != other.m_impl->type()) return false; return m_impl->equals(other.m_impl.get()); } bool operator!=(const Callback& other) const { return !(*this == other); } // 调用回调 bool operator()(const MyClass& arg) const { return m_impl->invoke(arg); } private: // 抽象基类,定义统一接口 struct Impl { virtual ~Impl() = default; virtual std::type_index type() const = 0; virtual bool invoke(const MyClass&) const = 0; virtual bool equals(const Impl*) const = 0; }; // 静态函数的具体实现 struct FuncImpl : Impl { bool(*func)(const MyClass&); FuncImpl(bool(*f)(const MyClass&)) : func(f) {} std::type_index type() const override { return std::type_index(typeid(FuncImpl)); } bool invoke(const MyClass& arg) const override { return func(arg); } bool equals(const Impl* other) const override { auto o = static_cast<const FuncImpl*>(other); return func == o->func; } }; // 成员函数的具体实现 template <typename T> struct MemFuncImpl : Impl { T* obj; bool(T::*mem_func)(const MyClass&); MemFuncImpl(T* o, bool(T::*mf)(const MyClass&)) : obj(o), mem_func(mf) {} std::type_index type() const override { return std::type_index(typeid(MemFuncImpl<T>)); } bool invoke(const MyClass& arg) const override { return (obj->*mem_func)(arg); } bool equals(const Impl* other) const override { auto o = static_cast<const MemFuncImpl<T>*>(other); return obj == o->obj && mem_func == o->mem_func; } }; // Lambda的具体实现 template <typename F> struct LambdaImpl : Impl { F func; LambdaImpl(F f) : func(std::move(f)) {} std::type_index type() const override { return std::type_index(typeid(LambdaImpl<F>)); } bool invoke(const MyClass& arg) const override { return func(arg); } bool equals(const Impl* other) const override { auto o = static_cast<const LambdaImpl<F>*>(other); return func == o->func; } }; std::unique_ptr<Impl> m_impl; };
使用时,静态函数、绑定的成员函数、带捕获的lambda都可以包装成Callback对象,直接存入std::vector<Callback>,并通过operator==进行相等比较,适配std::find等算法。
注意:C20之前,带捕获的lambda默认不支持
==操作;C20及以后,同类型的无捕获lambda支持==,若捕获的对象本身支持比较,也可以正常使用该逻辑。
方案2:为每个回调分配唯一ID,通过ID管理
如果不需要真正比较回调的逻辑,仅需实现注册去重和移除,可以为每个回调分配唯一标识,通过ID来操作:
#include <vector> #include <functional> #include <unordered_map> class CallbackNotifier { public: using Callback = std::function<bool(const MyClass&)>; using CallbackID = std::size_t; // 注册回调,返回唯一ID CallbackID registerCallback(Callback cb) { CallbackID id = m_nextID++; m_callbacks[id] = std::move(cb); return id; } // 通过ID移除回调 bool unregisterCallback(CallbackID id) { return m_callbacks.erase(id) > 0; } // 判断ID对应的回调是否已注册 bool isRegistered(CallbackID id) const { return m_callbacks.find(id) != m_callbacks.end(); } // 通知所有回调 void notify(const MyClass& arg) { for (const auto& [id, cb] : m_callbacks) { cb(arg); } } private: std::unordered_map<CallbackID, Callback> m_callbacks; CallbackID m_nextID = 0; };
这种方式实现简单、性能高效,客户端只需要保存注册时返回的ID即可完成后续操作,无需处理复杂的相等比较逻辑。缺点是无法通过回调对象本身进行查找,必须依赖ID。
内容的提问来源于stack exchange,提问作者ags

