在 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 中,它将在收到通知时设置一个事件。我的问题是如何同步它。我写了这样的东西:

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;
};

但我担心在 construction\destruction 期间调用回调(也许在这个特定示例中,shared_ptr 会很好,但可能与其他构造的 classes它不会是一样的)。

我再说一遍——我不想要一个非常具体的解决方案来解决这个非常具体的问题class,而是在传递回调时为 RAII 提供一个更通用的解决方案。

AFAICT,您担心 my_notif_callback 可以与析构函数并行调用,而 context 可以是悬空指针。这是一个合理的担忧,我认为您无法通过简单的锁定机制解决它。

相反,您可能需要结合使用共享指针和弱指针来避免此类悬空指针。例如,要解决您的问题,您可以将事件存储在 widget 中,这是一个 shared_ptr,然后您可以创建一个 weak_ptrwidget 并将其作为register_for_notif.

的上下文

换句话说,NotifClassshare_ptrWidget,上下文是 weak_ptrWidget。如果您无法锁定 weak_ptr,则 class 已被破坏:

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_ptrevent 作为上下文:

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;
};

您可以通过对静态容器的线程安全访问来执行此操作,该静态容器包含指向您的活动实例的指针。 RAII class 构造函数将 this 添加到容器中,析构函数将其删除。回调函数检查容器的上下文,如果不存在则检查 returns。它看起来像这样(未测试):

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_;
};

版主警告:为求本人,删除此post,直接编辑即可!

确保回调对象在注册之前已完全构建。意思是,使回调对象成为一个单独的 class,而 registration/deregistration 包装器成为一个单独的 class。 然后,您可以将两个 class 都链接到一个成员或基础 class 关系中。

struct A
{       CCallBackObject m_sCallback;
        CRegistration m_sRegistration;
        A(void)
             :m_sCallback(),
             m_sRegistration(&m_sCallback)
       {
       }
};

作为额外的好处,您可以重复使用 register/unregister 包装器...

如果回调可能发生在另一个线程中,我会重新设计这个软件以避免这种情况。 例如。可以让主线程的关闭(例如这个对象的破坏)等到所有工作线程都是 shutdown/finished.

您对同步的担忧有点不对。

总结一下你的问题,你有一些库,你可以用它注册一个回调函数和(通过 void* 指针或类似的)一些资源,函数通过 register() 函数作用于这些资源。这个库还提供了一个 unregister() 函数。

在您的代码中,您既不能也不应该尝试防止库在通过 unregister() 函数注销之后或注销时调用您的回调函数的可能性:图书馆有责任 确保在注销期间或注销后不会触发回调。图书馆应该担心同步、互斥和其他 gubbins,而不是你。

您的代码的两个职责是:

  • 确保在注册之前构建回调作用的资源,并且
  • 确保在销毁回调所作用的资源之前注销回调。

这种构造与破坏的相反顺序正是 C++ 对其成员变量所做的,以及为什么编译器会在您以 'wrong' 顺序初始化它们时警告您。

就您的示例而言,您需要确保 1) register_for_notif() 在共享指针初始化后调用,2) unregister_for_notif() 在 std::shared_ptr 之前调用(或无论什么)被摧毁。

后者的关键是理解析构函数中的析构顺序。如需回顾,请查看以下 cppreference.com page.

的 "Destruction sequence" 部分
  • 首先执行析构函数体;
  • 然后编译器以声明的相反顺序为 class 的所有非静态非变体成员调用析构函数。

因此,您的示例代码是 "safe"(或尽可能安全),因为 unregister_for_notif() 在析构函数体中被调用,在成员变量 [=17 被破坏之前=].

另一种(在某种意义上更明确地遵循 RAII)方法是将通知句柄与回调函数在其上运行的资源分开,方法是将其拆分为自己的 class。例如。类似于:

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回调有两种常见的通用解决方案。一个是对象的 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