【问题标题】:Slim c++ signal / event mechanism with move semantics for slots具有插槽移动语义的 Slim C++ 信号/事件机制
【发布时间】:2013-01-22 13:24:42
【问题描述】:

我正在尝试用 C++ 设计一个信号和槽系统。该机制在某种程度上受到 boost::signal 的启发,但应该更简单。我正在使用 MSVC 2010,这意味着一些 c++11 功能可用,但遗憾的是可变参数模板不可用。

首先,让我提供一些上下文信息。我实现了一个系统来处理由连接到 pc 的不同硬件传感器生成的数据。每个单独的硬件传感器都由一个从通用类 Device 继承的类表示。每个传感器都作为一个单独的线程运行,它接收数据并将其转发到多个 Processor 类(例如过滤器、可视化器等)。换句话说,设备是信号,处理器是插槽或侦听器。整个信号/插槽系统应该非常高效,因为传感器会生成大量数据。

以下代码显示了我对带有一个参数的信号的第一种方法。可以添加(复制)更多模板特化以包括对更多参数的支持。到目前为止,以下代码中缺少线程安全性(需要互斥锁来同步对 slot_vec 的访问)。

我想确保插槽的每个实例(即处理器实例)不能被另一个线程使用。因此我决定使用 unique_ptr 和 std::move 来实现插槽的移动语义。这应该确保当且仅当插槽断开连接或信号被破坏时,插槽​​也会被破坏。

我想知道这是否是一种“优雅”的方法。任何使用下面 Signal 类的类现在都可以创建 Signal 的实例或从 Signal 继承以提供典型的方法(即连接、发射等)。

#include <memory>
#include <utility>
#include <vector>

template<typename FunType>
struct FunParams;

template<typename R, typename A1>
struct FunParams<R(A1)>
{
    typedef R Ret_type;
    typedef A1 Arg1_type;
};

template<typename R, typename A1, typename A2>
struct FunParams<R(A1, A2)>
{
    typedef R Ret_type;
    typedef A1 Arg1_type;
    typedef A2 Arg2_type;
};


/**
Signal class for 1 argument.
@tparam FunSig Signature of the Signal
*/
template<class FunSig>
class Signal
{
public:
    // ignore return type -> return type of signal is void
    //typedef typenamen FunParams<FunSig>::Ret_type Ret_type;
    typedef typename FunParams<FunSig>::Arg1_type Arg1_type;

    typedef typename Slot<FunSig> Slot_type;

public:
    // virtual destructor to allow subclassing
    virtual ~Signal()
    {
        disconnectAllSlots();
    }

    // move semantics for slots
    bool moveAndConnectSlot(std::unique_ptr<Slot_type> >& ptrSlot)
    {
        slotsVec_.push_back(std::move(ptrSlot));
    }

    void disconnectAllSlots()
    {
        slotsVec_.clear();
    }

    // emit signal
    void operator()(Arg1_type arg1)
    {
        std::vector<std::unique_ptr<Slot_type> >::iterator iter = slotsVec_.begin();
        while (iter != slotsVec_.end())
        {
            (*iter)->operator()(arg1);
            ++iter;
        }
    }

private:
    std::vector<std::unique_ptr<Slot_type> > slotsVec_;

};


template <class FunSig>
class Slot
{
public:
    typedef typename FunParams<FunSig>::Ret_type Ret_type;
    typedef typename FunParams<FunSig>::Arg1_type Arg1_type;

public:
    // virtual destructor to allow subclassing
    virtual ~Slot() {}

    virtual Ret_type operator()(Arg1_type) = 0;
};

有关此方法的其他问题:

1) 通常信号和槽将使用对复杂数据类型的 const 引用作为参数。使用 boost::signal 需要使用 boost::cref 来提供参考。我想避免这种情况。如果我如下创建一个 Signal 实例和一个 Slot 实例,是否保证参数作为 const refs 传递?

class Sens1: public Signal<void(const float&)>
{
  //...
};

class SpecSlot: public Slot<Sens1::Slot_type>
{
   void operator()(const float& f){/* ... */}
};

Sens1 sens1;
sens1.moveAndConnectSlot(std::unique_ptr<SpecSlot>(new SpecSlot));
float i;
sens1(i);

2) boost::signal2 不需要槽类型(接收器不必从通用槽类型继承)。实际上可以连接任何函子或函数指针。这实际上是如何工作的?如果 boost::function 用于将任何函数指针或方法指针连接到信号,这可能很有用。

【问题讨论】:

  • 你看过sigslot吗?
  • 对于 slot 的生命周期管理,boost::signals2 提供了一个方法 slot::track。见here

标签: c++ templates boost signals-slots


【解决方案1】:

前提:

如果您打算在大型项目或生产项目中使用它,我的第一个建议是不要重新发明轮子,而是使用 Boost.Signals2 或替代库。这些库并不像您想象的那么复杂,并且可能比您想出的任何 ad hoc 解决方案都更有效。

这就是说,如果您的目标更像是一种说教,并且您想尝试一下这些东西以弄清楚它们是如何实现的,那么我很欣赏您的精神,并会尝试回答您的问题,但在给您一些改进建议之前不要这样做。

建议:

首先,这句话让人迷惑:

"到目前为止,connect 和 disconnect 方法都不是线程安全的。但是我想确保插槽的每个实例(即处理器实例)不能被另一个线程使用. 因此我决定使用unique_ptrstd::move 来实现槽的移动语义"

以防万一您正在考虑它(您句子中的“但是”暗示了这一点),使用 unique_ptr 并不能真正使您不必保护您的插槽 vector 免受数据竞争的影响。因此,无论如何,您仍然应该使用互斥锁来同步对slots_vec 的访问。

第二点:通过使用unique_ptr,您将插槽对象的独占所有权授予单个信号对象。如果我理解正确,您声称您这样做是为了避免不同的线程弄乱同一个插槽(这将迫使您同步对它的访问)。

我不确定从设计角度来看,这是一个合理的选择。首先,不可能为多个信号注册同一个槽(我听说你反对说你现在不需要那个,但请坚持住)。其次,您可能希望在运行时更改这些处理器的状态,以便调整它们对接收到的信号的反应。但是如果你没有指向它们的指针,你会怎么做呢?

就个人而言,我至少会选择shared_ptr,这将允许自动管理您的插槽的生命周期;如果你不想让多个线程搞乱这些对象,就不要让它们访问它们。只需避免将共享指针传递给这些线程。

但我什至会更进一步:如果您的插槽是可调用对象,看起来确实如此,那么我将完全放弃shared_ptr而是使用std::function&lt;&gt; 将它们封装在Signal 类中。也就是说,我只保留 vectorstd::function&lt;&gt; 对象,以便在每次发出信号时调用。这样,您将拥有更多的选择,而不仅仅是从 Slot 继承来设置回调:您可以注册一个简单的函数指针,或者 std::bind 的结果,或者只是您可以想出的任何仿函数(甚至是λ)。

现在您可能会发现这与 Boost.Signals2 的设计非常相似。请不要以为我没有忽略您最初的设计目标是拥有比这更纤薄的东西。我只是想向你展示为什么一个最先进的图书馆是这样设计的,以及为什么最终诉诸它是有意义的。

当然,在 Signal 类中注册 std::function 对象而不是智能指针会迫使您注意在堆上分配的函子的生命周期;但是,这不一定必须是Signal 类的责任。您可以为此目的创建一个包装类,它可以保留指向您在堆上创建的仿函数的共享指针(例如从Slot 派生的类的实例)并将它们注册到Signal 对象。通过一些调整,这还允许您单独注册和断开插槽,而不是“全部或全部”。

答案:

但是现在让我们假设您的要求是并且将永远是(后半部分真的很难预见)确实是这样的:

  1. 您不需要为多个信号注册同一个插槽;
  2. 您无需在运行时更改槽的状态;
  3. 您不需要注册不同类型的回调(lambda、函数指针、仿函数...);
  4. 您无需选择性地断开各个插槽。

那么这里是你的问题的答案:

Q1:“[...] 如果我如下创建一个 Signal 实例和一个 Slot 实例,是否保证参数作为 const refs 传递?”

A1:是的,它们将作为常量引用传递,因为沿着您的转发路径的所有内容都是常量引用。

Q2:"[在 Boost.Signals2 中] 实际上可以连接任何函子或函数指针。这实际上是如何工作的?如果 boost::function 用于连接任何函数指针或方法指针,这可能很有用到一个信号"

A2:它基于boost::function&lt;&gt; 类模板(后来变成std::function,如果我没记错的话,应该在VS2010 中得到支持),它使用type erasure techniques 来包装不同类型的可调用对象,但相同的签名。如果您对实现细节感到好奇,请查看implementation of boost::function&lt;&gt; 或查看MS 对std::function&lt;&gt; 的实现(应该非常相似)。

我希望这对您有所帮助。如果没有,请随时在 cmets 中提出其他问题。

【讨论】:

  • 非常感谢您详细而详细的回答。您完全正确,应该使用互斥锁来同步对 slot_vec 的访问。我编辑了我的问题以进行澄清。
  • 你也对,通常应该使用现有的实现。关于使用哪个信号/槽库的讨论可以找到herehere
  • 到目前为止我一直在使用 boost 的 signal2 库,但它因性能低下而受到批评。我自己实现的主要原因是我使用智能指针来管理信号和槽。但是 boost::signal 的 connect() 不适用于(智能)指针。因此我不得不取消引用指向插槽的指针来连接它:mysig.connect(*sharedPtr_to_my_slot)。这有点难看,如果引用计数为 0 并且信号可能仍使用插槽...
【解决方案2】:

这是我的方法:

它比 boost 轻得多,但不处理聚合响应。

我认为将 shared_ptr 用于回调的所有者,将weak_ptr 用于信号引发器,这样可以确保回调仍然存在。

我也喜欢它自我清除已死的weak_ptr回调的方式。

template <typename... FuncArgs>
class Signal
{
    using fp = std::function<void(FuncArgs...)>;
    std::forward_list<std::weak_ptr<fp> > registeredListeners;
public:
    using Listener = std::shared_ptr<fp>;

    Listener add(const std::function<void(FuncArgs...)> &cb) {
        // passing by address, until copy is made in the Listener as owner.
        Listener result(std::make_shared<fp>(cb));
        registeredListeners.push_front(result);
        return result;
    }

    void raise(FuncArgs... args) {
        registeredListeners.remove_if([&args...](std::weak_ptr<fp> e) -> bool {
            if (auto f = e.lock()) {
                (*f)(args...);
                return false;
            }
            return true;
        });
    }
};

用法:

Signal<int> bloopChanged;

// ...

Signal<int>::Listener bloopResponse = bloopChanged.add([](int i) { ... });

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-09-27
    • 2016-11-18
    • 2015-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多