Event System (C++) – Consumable Events

This article is a continuation of my previous article, where I wrote about event systems. Reading it it not required to understand this article, but it might explain some things I mention.

Consumable Events

A possibility for an event system is to make events consumable. That is, a certain delegate can tell the event system to stop sending out the event it just received as other delegates shouldn’t be handling it any more. A use case for this could be an input system.
I’m not talking about this as (an attempt at) optimisation, though this might be possible as well.

This requires some considerations.
1. The event broadcasting order needs to be guaranteed. The order not being guaranteed opens up the possibility of the event being stopped by the wrong delegate.
2. Delegates need a way of telling the event that it was consumed. One way of doing this is having all delegates return a boolean (rather than void). However, what if you don’t want an event to be consumable?

For 1, using a sequential container is part of the solution. Giving the user control over the order itself might be a bit more difficult.
Something that could be done here is a bit of a compromise: Let users choose a priority level for their events. When an event is consumed, it doesn’t reach delegates with a lower priority level, while still reaching delegates at the same level.

For 2, you could use a boolean that indicates if an event is consumable or not. If it is, check the return value of the delegates. If it’s not, don’t check it.
All delegates would have to return a boolean like this, and all delegate function signatures would be somewhat similar (bool Delegate(EventInfo)). This doesn’t clearly show the user whether events can be consumed or not.
You could add the return type (bool or void) as template parameter, but there’s a problem with this: you can’t check the return value of a function returning void in an if-statement. So, you’d need to implement the Broadcast-function in two different ways.

Partial Specialization

Partial specialization allows you to customise the implementation of a struct or class for specific template arguments. That said, partial specialization only works for classes and structs. Not functions, as those have to be explicitly (or fully) specialized. You can read why this is the case in this article by Herb Sutter.

As this means we can’t use partial specialization to solve this, do we have to write a second class with the exact same code (except for a single return value that’s not checked in one of the two)? Luckily, there’s another (possible) solution: std::enable_if.

std::enable_if

std::enable_if can be used to leverage the concept of SFINAE in order to conditionally remove functions. If the condition passed to std::enable_if is false, the function declaration it is used in is ill-formed and discarded. Using this allows us to have one function implementation for a non-consumable event, and one for a consumable event without implementing the entire class twice.

std::enable_if can’t be used with a template type specified at class-level, but has to use a function-level template type (explained here). To deal with this, it’s possible to use a kind of hacky solution:

template<typename EventInfoType, typename ReturnType>
class EventBase
{
public:
    template<typename T = ReturnType>
    void Broadcast(EventInfoType a_Event);
    // Rest of the class
};

Actually using std::enable_if here:

template<typename EventInfoType, typename ReturnType>
class EventBase
{
public:
    template<typename T = ReturnType>
    typename std::enable_if<std::is_void<T>::value>::type Broadcast(EventInfoType a_Event);
    template<typename T = ReturnType>
    typename std::enable_if<!std::is_void<T>::value>::type Broadcast(EventInfoType a_Event);
    // Rest of the class
};

This results in a single Broadcast function being defined per event, as ReturnType can’t be void and not void at the same time. Now, two variations of the Broadcast-function can be implemented without implementing the entire class twice.

Note that I am assuming that any type other than void is bool-convertible. You’ll probably want to limit ReturnType to void and bool anyway, and possible ways of doing that include using a static_assert or a templated constexpr bool, which would be the same as the check done in the first enable_if template argument.

// In a function
static_assert(std::is_same<T, void>::value);
// Outside of the class
template<typename T>
constexpr bool is_void_v = std::is_same<T, void>::value;

The enable_if can also be placed in the template parameter list, if you prefer. Using a constexpr bool like is_void_v:

template<typename EventInfoType, typename ReturnType>
class EventBase
{
public:
    template<typename T = ReturnType, class = std::enable_if<is_void_v<T>>::type>
    void Broadcast(EventInfoType a_Event);
    template<typename T = ReturnType, class = std::enable_if<!is_void_v<T>>::type>
    void Broadcast(EventInfoType a_Event);
    // Rest of the class
};

When doing this, the function definition would look something like this:

template<typename T, class _Enabled>
    void Broadcast(EventInfoType a_Event)
{
    // Implementation
}

If Constexpr

When using C++17 or higher, if constexpr is a thing. You can have a single public Broadcast function, while having two (private) implementations:

template<typename EventInfoType, typename ReturnType>
class EventBase
{
public:
    void Broadcast(EventInfoType a_Event)
    {
        if constexpr (std::is_void<ReturnType>::value)
        {
            BroadcastRegularEvent(a_Event);
        }
        else
        {
            BroadcastConsumableEvent(a_Event);
        }
    }
private:
    void BroadcastConsumableEvent(EventInfoType a_Event);
    void BroadcastRegularEvent(EventInfoType a_Event);
    // Rest of the class
};

Tag Dispatching

If if constexpr is not an option, you can also use tag dispatching. A “tag” is a type that has no behaviour and no data.
Tag dispatching requires an extra argument in the internal functions, so it’s simply an application of function overloading.

template<typename EventInfoType, typename ReturnType>
class EventBase
{
public:
    void Broadcast(EventInfoType a_Event)
    {
        Broadcast_Impl(a_Event, std::bool_constant<!std::is_void<ReturnType>::value>());
    }
private:
    // Consumable event
    void Broadcast_Impl(EventInfoType a_Event, std::true_type);
    // Non-consumable event
    void Broadcast_Impl(EventInfoType a_Event, std::false_type);
    // Rest of the class
};

In this case, only two tags are needed. When there’s more than two options, you can use empty structs or classes instead.

Conclusion

When working on an event system, you might just want to choose between having all events be consumable or having none be. The latter is probably easier, as it takes away the need for delegate ordering and prevents events from “disappearing” (i.e. being consumed by a delegate that shouldn’t consume it). It also results in there only being one return type for delegates, removing needless complication for the user (who would have to find out what return type is expected).

Leave a Reply

Your email address will not be published. Required fields are marked *