在 c++ 中使用 RAII 进行回调注册
Using RAII for callback registration in c++
我正在使用一些API来获取通知。像这样:
NOTIF_HANDLE register_for_notif(CALLBACK func, void* context_for_callback);
void unregister_for_notif(NOTIF_HANDLE notif_to_delete);
我想把它包装在一些体面的 RAII 类中,该类将在收到通知时设置一个事件。我的问题是如何同步它。我写了这样的东西:
class NotifClass
{
public:
NotifClass(std::shared_ptr<MyEvent> event):
_event(event),
_notif_handle(register_for_notif(my_notif_callback, (void*)this))
// initialize some other stuff
{
// Initialize some more stuff
}
~NotifClass()
{
unregister_for_notif(_notif_handle);
}
void my_notif_callback(void* context)
{
((NotifClass*)context)->_event->set_event();
}
private:
std::shared_ptr<MyEvent> _event;
NOTIF_HANDLE _notif_handle;
};
但是我担心在构造\销毁期间调用回调(也许在这个特定示例中,shared_ptr会很好,但对于其他构造类,它可能会有所不同)。
我会再说一遍 - 我不想要这个非常具体的类的非常具体的解决方案,而是在传递回调时为 RAII 提供更通用的解决方案。
你对同步的担忧有点错位。
为了总结您的问题,您有一些库,您可以使用它注册回调函数,以及(通过 void* 指针或类似)一些资源,该函数通过register()
函数对其进行操作。该库还提供unregister()
函数。
在您的代码中,您既不能也不应该尝试防止库可以在通过unregister()
函数取消注册后或取消注册时调用您的回调函数的可能性:库有责任确保回调在取消注册时或取消注册后无法触发。图书馆应该担心同步、互斥体和其他的 gubbins,而不是你。
代码的两个职责是:
- 确保在注册回调之前构造回调作用的资源,并且
- 请确保在销毁回调所针对的资源之前取消注册回调。
构造与破坏的这种反顺序正是C++对其成员变量所做的,也是编译器在以"错误"顺序初始化它们时警告您的原因。
就您的示例而言,您需要确保 1)register_for_notif()
在初始化共享指针后调用,2) 在 std::shared_ptr(或其他任何内容)被销毁之前调用 2)unregister_for_notif()
。
后者的关键是理解析构函数中的销毁顺序。有关回顾,请查看以下 cppreference.com 页面的"销毁序列"部分。
- 首先,执行析构函数的主体;
- 然后,编译器按与声明相反的顺序调用类的所有非静态非变量成员的析构函数。
因此,您的示例代码是"安全的"(或尽可能安全),因为在销毁成员变量std::shared_ptr<MyEvent> _event
之前,在析构函数主体中调用unregister_for_notif()
。
另一种(在某种意义上更明确地遵循 RAII)方法是通过将通知句柄拆分为自己的类来将通知句柄与回调函数运行的资源分开。 例如:
class NotifHandle {
public:
NotifHandle(void (*callback_fn)(void *), void * context)
: _handle(register_for_notif(callback_fn, context)) {}
~NotifHandle() { unregister_for_notif(_handle); }
private:
NOTIF_HANDLE _handle;
};
class NotifClass {
public:
NotifClass(std::shared_ptr<MyEvent> event)
: _event(event),
_handle(my_notif_callback, (void*)this) {}
~NotifClass() {}
static void my_notif_callback(void* context) {
((NotifClass*)context)->_event->set_event();
}
private:
std::shared_ptr<MyEvent> _event;
NotifHandle _handle;
};
重要的是成员变量声明顺序:NotifHandle _handle
是在资源std::shared_ptr<MyEvent> _event
之后声明的,因此保证在销毁资源之前取消注册通知。
您可以通过对静态容器的线程安全访问来执行此操作,该静态容器包含指向实时实例的指针。RAII 类构造函数将this
添加到容器中,析构函数将其删除。回调函数根据容器检查上下文,如果上下文不存在,则返回。它将看起来像这样(未经测试):
class NotifyClass {
public:
NotifyClass(const std::shared_ptr<MyEvent>& event)
: event_(event) {
{
// Add to thread-safe collection of instances.
std::lock_guard<std::mutex> lock(mutex_);
instances_.insert(this);
}
// Register the callback at the end of the constructor to
// ensure initialization is complete.
handle_ = register_for_notif(&callback, this);
}
~NotifyClass() {
unregister_for_notif(handle_);
{
// Remove from thread-safe collection of instances.
std::lock_guard<std::mutex> lock(mutex_);
instances_.erase(this);
}
// Guaranteed not to be called from this point so
// further destruction is safe.
}
static void callback(void *context) {
std::shared_ptr<MyEvent> event;
{
// Ignore if the instance does not exist.
std::lock_guard<std::mutex> lock(mutex_);
if (instances_.count(context) == 0)
return;
NotifyClass *instance = static_cast<NotifyClass*>(context);
event = instance->event_;
}
event->set_event();
}
// Rule of Three. Implement if desired.
NotifyClass(const NotifyClass&) = delete;
NotifyClass& operator=(const NotifyClass&) = delete;
private:
// Synchronized associative container of instances.
static std::mutex mutex_;
static std::unordered_set<void*> instances_;
const std::shared_ptr<MyEvent> event_;
NOTIF_HANDLE handle_;
};
请注意,回调会在使用共享指针之前递增共享指针并释放容器上的锁。这可以防止在触发MyEvent
可以同步创建或销毁NotifyClass
实例时出现潜在的死锁。
从技术上讲,上述操作可能会由于地址重用而失败。也就是说,如果一个NotifyClass
实例被销毁,并且立即在完全相同的内存地址上创建一个新实例,那么可以想象,针对旧实例的 API 回调可能会传递到新实例。对于某些用法,甚至大多数用法,这无关紧要。如果它确实很重要,则必须使静态容器键全局唯一。这可以通过将集合替换为映射并传递映射键而不是指向 API 的指针来完成,例如:
class NotifyClass {
public:
NotifyClass(const std::shared_ptr<MyEvent>& event)
: event_(event) {
{
// Add to thread-safe collection of instances.
std::lock_guard<std::mutex> lock(mutex_);
key_ = nextKey++;
instances_[key_] = this;
}
// Register the callback at the end of the constructor to
// ensure initialization is complete.
handle_ = register_for_notif(&callback, reinterpret_cast<void *>(key_));
}
~NotifyClass() {
unregister_for_notif(handle_);
{
// Remove from thread-safe collection of instances.
std::lock_guard<std::mutex> lock(mutex_);
instances_.erase(key_);
}
// Guaranteed not to be called from this point so
// further destruction is safe.
}
static void callback(void *context) {
// Ignore if the instance does not exist.
std::shared_ptr<MyEvent> event;
{
std::lock_guard<std::mutex> lock(mutex_);
uintptr_t key = reinterpret_cast<uintptr_t>(context);
auto i = instances_.find(key);
if (i == instances_.end())
return;
NotifyClass *instance = i->second;
event = instance->event_;
}
event->set_event();
}
// Rule of Three. Implement if desired.
NotifyClass(const NotifyClass&) = delete;
NotifyClass& operator=(const NotifyClass&) = delete;
private:
// Synchronized associative container of instances.
static std::mutex mutex_;
static uintptr_t nextKey_;
static std::unordered_map<unsigned long, NotifyClass*> instances_;
const std::shared_ptr<MyEvent> event_;
NOTIF_HANDLE handle_;
uintptr_t key_;
};
RAII 回调有两种常见的通用解决方案。 一个是对象shared_ptr
的公共接口。 另一个是std::function
.
使用通用接口允许一个smart_ptr控制对象所有回调的生存期。 这类似于观察者模式。
class Observer
{
public:
virtual ~Observer() {}
virtual void Callback1() = 0;
virtual void Callback2() = 0;
};
class MyEvent
{
public:
void SignalCallback1()
{
const auto lock = m_spListener.lock();
if (lock) lock->Callback1();
}
void SignalCallback2()
{
const auto lock = m_spListener.lock();
if (lock) lock->Callback2();
}
void RegisterCallbacks(std::shared_ptr<Observer> spListener)
{
m_spListener = spListener;
}
private:
std::weak_ptr<Observer> m_spListener;
};
class NotifClass : public Observer
{
public:
void Callback1() { std::cout << "NotifClass 1" << std::endl; }
void Callback2() { std::cout << "NotifClass 2" << std::endl; }
};
示例使用。
MyEvent source;
{
auto notif = std::make_shared<NotifClass>();
source.RegisterCallbacks(notif);
source.SignalCallback1(); // Prints NotifClass 1
}
source.SignalCallback2(); // Doesn't print NotifClass 2
如果使用 C 样式的成员指针,则必须担心对象的地址和成员回调。std::function
可以用lambda很好地封装这两件事。 这允许您单独管理每个回调的生存期。
class MyEvent
{
public:
void SignalCallback()
{
const auto lock = m_spListener.lock();
if (lock) (*lock)();
}
void RegisterCallback(std::shared_ptr<std::function<void(void)>> spListener)
{
m_spListener = spListener;
}
private:
std::weak_ptr<std::function<void(void)>> m_spListener;
};
class NotifClass
{
public:
void Callback() { std::cout << "NotifClass 1" << std::endl; }
};
示例使用。
MyEvent source;
// This doesn't need to be a smart pointer.
auto notif = std::make_shared<NotifClass>();
{
auto callback = std::make_shared<std::function<void(void)>>(
[notif]()
{
notif->Callback();
});
notif = nullptr; // note the callback already captured notif and will keep it alive
source.RegisterCallback(callback);
source.SignalCallback(); // Prints NotifClass 1
}
source.SignalCallback(); // Doesn't print NotifClass 1
AFAICT,您担心my_notif_callback
可以与析构函数并行调用,context
可以是悬而未决的指针。这是一个合理的担忧,我认为你不能用简单的锁定机制来解决它。
相反,您可能需要使用共享指针和弱指针的组合来避免此类悬空指针。例如,要解决您的问题,您可以将事件存储在widget
(shared_ptr
中),然后您可以创建对widget
的weak_ptr
并将其作为上下文传递给register_for_notif
。
换句话说,NotifClass
对Widget
具有share_ptr
,上下文是对Widget
的weak_ptr
。如果无法锁定weak_ptr
则类已被销毁:
class NotifClass
{
public:
NotifClass(const std::shared_ptr<MyEvent>& event):
_widget(std::make_shared<Widget>(event)),
_notif_handle(register_for_notif(my_notif_callback, (void*)new std::weak_ptr<Widget>(_widget)))
// initialize some other stuff
{
// Initialize some more stuff
}
~NotifClass()
{
unregister_for_notif(_notif_handle);
}
static void my_notif_callback(void* context)
{
auto ptr = ((std::weak_ptr<Widget>*)context)->lock();
// If destructed, do not set the event.
if (!ptr) {
return;
}
ptr->_event->set_event();
}
private:
struct Widget {
Widget(const std::shared_ptr<MyEvent>& event)
: _event(event) {}
std::shared_ptr<MyEvent> _event;
};
std::shared_ptr<Widget> _widget;
NOTIF_HANDLE _notif_handle;
};
请注意,要添加到NotifClass
中的任何功能实际上都应该进入Widget
.如果没有此类额外功能,可以跳过Widget
间接寻址并使用weak_ptr
event
作为上下文:
class NotifClass
{
public:
NotifClass(const std::shared_ptr<MyEvent>& event):
_event(event),
_notif_handle(register_for_notif(my_notif_callback, (void*)new std::weak_ptr<MyEvent>(event)))
// initialize some other stuff
{
// Initialize some more stuff
}
~NotifClass()
{
unregister_for_notif(_notif_handle);
}
static void my_notif_callback(void* context)
{
auto ptr = ((std::weak_ptr<MyEvent>*)context)->lock();
// If destructed, do not set the event.
if (!ptr) {
return;
}
ptr->set_event();
}
private:
std::shared_ptr<MyEvent> _event;
NOTIF_HANDLE _notif_handle;
};
版主警告:为了请求我删除此帖子,只需编辑它!
在注册回调对象之前,请确保回调对象已完全构造。意味着,使回调对象成为单独的类,使注册/取消注册包装器成为单独的类。 然后,可以将这两个类链接到成员或基类关系中。
结构 A { CCallBackObject m_sCallback; 注册m_sRegistration; A(无效) :m_sCallback(), m_sRegistration(&m_sCallback) { } };
作为另一个好处,您可以重复使用注册/取消注册包装器...
如果回调可能发生在另一个线程中,我会重新设计这个软件以避免这种情况。 例如,可以使主线程的关闭(例如此对象的破坏)等到所有工作线程都关闭/完成。
- 关于回调和注册回调的混淆 (C++)
- 如何在SOCI中注册数据库故障转移回调?
- 如何将从本机 c++ 的回调注册到 c#
- 如何防止 C API 注册表中的 Lua 回调被垃圾回收?
- 注册和调用 DLL 回调的正确方法是什么
- 共享指针和回调注册的结构.由于我之外的原因调用回调时,原始指针值发生了变化
- 在 Linux 中退出时如何注册进程的回调
- 用于回调注册的 SFINAE
- Openssl 线程安全回调函数注册,包括直接调用和间接调用
- 在 c++ 中使用 RAII 进行回调注册
- 注册用于提升io_service的每个对象的回调函数是什么?
- Cython:在DLL中注册用于回调的Python函数
- 注册回调时不进行输出
- 在clCreateContext()中注册的回调中用C++抛出异常是否安全
- 正在注销通过register_callback()注册的回调
- C++OOP注册回调函数(typedef问题)
- 创建一个注册回调的抽象基类的正确方法
- 在openGL中注册回调时出现编译错误
- std::函数作为回调,可以取消注册
- 使用std::bind将类成员函数注册为函数的回调