使用函数<void(boost :: any)=“”>好主意的事件系统?</void>

时间:2012-04-05 23:16:13

标签: c++ boost event-handling c++11 boost-any

我做了一个模块系统,如下所示:

//setting event
module->set_event("started", [](boost::any ev) {
  cout << "The module have been successfully started" << endl;
});

//somewhere else
module->start();

//impl
void Module::start() {
  //run once protection here
  this->trigger_event("start");//pre start
  this->_impl->start(); //on error, throw exception
  this->trigger_event("started"); //post start
}

void Module::trigger_event(string str,boost::any ev = boost::any() )
{
  //mutex
  //invokes every handler which have been invoked set_event
}

非常简单易懂。第一个缺点是没有“remove_event”,但允许它(如果我需要它)我可以只返回一个std :: function(在set_event方法中),它在调用时删除了处理程序,所以如果用户想要删除event,只需调用返回的处理程序:

function<void ()> h = module->set_event(...);
//... somewhere later
h() //removes the handler safely

但如果这是一个好的设计,我很好奇吗?或者我会突然发现使用我的方法有很大的缺点,需要重写大多数代码吗?我可以说“trigger_event”方法也将用作模块之间的消息系统,就像JavaScript库jQuery方法一样:“bind”允许。一个例子:

module->set_event("message", [](boost::any message) {
 //deal with the message here
});

//send message
module->trigger_event("message", MyMessageTypeHere);

在大多数情况下,这是一个公认的设计吗?我想要一个非常灵活的方法,因此我的想法是使用boost :: any,但这一般是一个好主意(可扩展性,性能,灵活性)吗?

2 个答案:

答案 0 :(得分:4)

我会改变一些事情。

首先,让我们忽略一点回报,并专注于与boost::any混合的std::string。这对于事件系统来说确实是个坏主意,因为它允许潜在的运行时类型不匹配。你只是不想要一个可以犯这些错误的设计(因为那时 最终会犯 错误 被修复,浪费未来时间。)

通常,您真正想要的是:

void Module::trigger_event(shared_ptr<Event> const& event)
{
    // Event is a base interface
    // pull off event->ID() to get identifier and lookup
    // dispatch to something that has the signature
    //   void (shared_ptr<SpecificEvent>)
}

要做一些像调度员这样的事情,你通常有

map<IDType, shared_ptr<Dispatcher>> dispatchDictionary_;

template <typename SpecificEvent>
class SpecificDispatcher : public Dispatcher
{
public:
    SpecificDispatcher(function<void (shared_ptr<SpecificEvent>)> handler)
        handler_(handler)
    {}

    virtual void dispatch(shared_ptr<Event> event)
    {
        auto specificEvent(static_ptr_cast<SpecificEvent>(event));

        handler_(specificEvent);
    }
};

(以明显的方式定义接口类,并注册/取消注册方法以关联地图中的事件类型ID)。关键是将ID与类中的EventType相关联,以确保它始终是相同的关联,并以处理程序不需要自己重新解释数据的方式进行调度(容易出错)。做可以在你的图书馆内自动完成的工作,所以在外面不会做错。

好的,现在你用你的方法返回什么?返回一个对象,而不是一个函数。如果他们不需要明确地调用它,那么您已经保存了另一个潜在的错误来源。就像所有RAII一样 - 你不希望人们不得不记得调用删除,解锁或者......同样地,使用“unregister_event”(或者你称之为的任何东西)。

在一个程序中,人们必须关注四个生命周期:表达式,功能范围,状态和程序。这些对应于它们可以存储您返回的对象的四种类型:匿名(让它落在地板上 - 它可以在表达式中使用但从不分配给命名对象),自动范围对象,对象它是两个异步转换事件之间存在的State类(在State模式中)的成员,或者是一个坐到出口的全局范围对象。

所有终身管理都应使用RAII进行管理。这是析构函数的重点,也是如何在异常情况下管理资源清理。


编辑:我还有一些没有说明的东西,指出你会以“显而易见的方式”连接点。但是我想我会填充更多的部分(因为我有一个操作系统构建和安装,我的bug现在已经下降到最后一个,等待安装...)

关键是有人应该能够输入

callbackLifetimeObject = module->set_event<StartEvent>([](shared_ptr<StartEvent> event){
    cout << "Module started: " << event->someInfo() << endl;
});

所以set_event需要具有这种签名类型,并且它应该将适当的调度程序插入到字典中。有几种方法可以从这里获取ID类型。 “显而易见”的方法是只创建一个临时的并调用它的“ID”成员 - 但这会产生对象创建开销。另一种方法是将其变为“虚拟静态”,然后获取静态(这也是所有虚拟方法都可以)。每当我有虚拟静态时,我倾向于将它们变成特征 - 同样的事情,但是稍微更好的封装,以及修改“已经关闭的类”的能力。然后虚拟方法也调用traits类来返回相同的东西。

因此...

template <typename EventType>
struct EventTrait
{
    // typedef your IDType from whatever - looks like you want std::string
    static IDType eventID()
    { /* default impl */ }
};

template <>
struct EventTrait<StartEvent>
{
    // same as above
    static IDType eventID()
    { return "Start"; }
};

然后你可以

template <typename EventType>
EventRegistration set_event(function<void (shared_ptr<EventType>)> handler)
{
    auto id(EventTrait<EventType>::eventID());

    dispatchDictionary_.insert(make_pair(id, 
        make_shared<SpecificDispatcher<EventType>>(handler)));

    return EventRegistration(bind(& Module::unset_event, this, id));
}

这应该说明如何将各个部分组合在一起以及您拥有的一些选项。其中一些说明了可能会在每个事件中重新编码的样板代码。这也是您可能希望自动化的类型。有许多先进的技术可以对这种生成进行元编程,从使用另一个构建步骤开始,该步骤将规范引入到c ++元编程系统中。同样,与之前的观点一样,您可以实现的自动化程度越高,您拥有的错误就越少。

答案 1 :(得分:1)

这有两个原因。

首先,您将事件限制为只接收一个参数的处理程序,而从您的用例判断,您可以拥有各种各样的事件。所以,这是限制性的(是的,你可能std::tuple抛出到任何类型中,以编码几个参数,但你真的想要吗?)。

更重要的是,减弱类型安全性还需要弱化(类型)安全性。如果你是事件处理者只能与山羊一起工作,不要给它一只羊羔。您想要的,似乎可以使用模板(至少在C ++ 11中),因为您可以使trigger_event成为一个类型函数,它接收任意数量的参数。然后它尝试用这个参数调用处理程序,这会产生编译错误,你没有提供正确的参数数量和类型。

这里,在您的调度程序中,您可以隐藏类型解析,这有点棘手,因为您想要存储任意类型的事件。这可能是使用any的地方,但是中保留了调度程序实现/逻辑,而不是泄漏给用户。

编辑:要实现此解决方案,我的方法是使用一些内部抽象Event类型(可以存储指针)和一些template <typename Hnd> classs ConcreteEvent : public Event,它允许您将客户的事件处理程序保留为ConcreteEvent<EventType>(假设EventTypeset_event方法的模板参数)。使用rtti,您可以稍后检查是否在检索它时(您只会将其检索为Event*),它与您要调用的类型相匹配。