1

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

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

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

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

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

#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 用于将任何函数指针或方法指针连接到信号,这可能很有用。

4

2 回答 2

2

前提:

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

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

建议:

首先,这句话令人困惑:

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

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

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

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

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

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

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

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

答案:

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

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

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

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

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

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

A2:它基于类模板(如果我没记错的话,boost::function<>它后来成为并且应该在 VS2010 中得到支持),它使用类型擦除技术来包装不同类型但签名相同的可调用对象。如果您对实现细节感到好奇,请查看实现或看看 MS 的实现(应该非常相似)。std::functionboost::function<>std::function<>

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

于 2013-01-22T14:25:37.247 回答
0

这是我的方法:

它比 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) { ... });
于 2019-10-10T21:20:34.687 回答