2

I know questions like this have been asked before, but I am yet to find an adequate answer to my specific situation.

Here it is: I am trying to build an event system, where the programmer can create events which can be broadcast by an event manager and be listened for by event listeners. Each event is derived from a common base class GameEvent.

#pragma once

#include <string>

struct GameEvent
{
    std::string name = "Game Event";
};

// Events
struct StartEvent : GameEvent
{
    std::string msg = "This is the start event!";
};

struct EndEvent : GameEvent
{
    std::string msg = "This is the end event";
};

struct Events
{
    static const StartEvent s_StartEvent;
    static const EndEvent s_EndEvent;
};

const StartEvent Events::s_StartEvent;
const EndEvent Events::s_EndEvent;

// Custom hash function for GameEvent.
struct GameEventHash
{
    std::size_t operator()(const GameEvent& evt) const
    {
        return std::hash<std::string>()(evt.name) << 1;
    }
};

// Custom equal function for GameEvent.
struct GameEventEqual
{
    bool operator()(const GameEvent& t1, const GameEvent& t2) const {
        return t1.name == t2.name;
    }
};

To store the associations with Events to their listeners, I am trying to use an unordered map of GameEvents to vectors of std::functions. This can be seen as a static member of the EventManager class.

#pragma once

#include "events.h"

#include <functional>
#include <unordered_map>
#include <vector>

struct EventManager
{
    template<typename T>
    static void AddListener(std::function<void(T)> listener);

    static void Broadcast(GameEvent evt);
private:
    static std::unordered_map<GameEvent, std::vector<std::function<void(GameEvent)>>, GameEventHash, GameEventEqual> s_Events;
};

inline void EventManager::Broadcast(GameEvent evt)
{
    for (std::function<void(GameEvent)> listener : s_Events[evt])
    {
        listener(evt);
    }
}

template<typename T>
void EventManager::AddListener(std::function<void(T)> listener)
{
    T evt;

    // Add function to map of events to listeners.
    auto iter = s_Events.find(evt);

    // If event already has listeners
    if (iter != s_Events.end())
    {
        // Add to the vector of listeners.
        std::vector<std::function<void(GameEvent)>>& listeners = s_Events[evt];
        listeners.push_back(listener);
    }
    else
    {
        // Insert new vector with listener.
        s_Events[evt] = std::vector<std::function<void(GameEvent)>>{listener};
    }

}

std::unordered_map<GameEvent, std::vector<std::function<void(GameEvent)>>, GameEventHash, GameEventEqual> EventManager::s_Events;

The problem I have is with the AddListener method of the EventManager class. Because the vector of the unordered map of events takes functions of type GameEvent, trying to use any other type in the template causes errors. The errors in question are:

EventSystem\EventSystem\event-manager.h(40,12): error C2664: 'void std::vector<std::function<void (GameEvent)>,std::allocator<std::function<void (GameEvent)>>>::push_back(const _Ty &)': cannot convert argument 1 from 'std::function<void (StartEvent)>' to 'const _Ty &'

and

EventSystem\EventSystem\event-manager.h(45,62): error C2440: '<function-style-cast>': cannot convert from 'initializer list' to 'std::vector<std::function<void (GameEvent)>,std::allocator<std::function<void (GameEvent)>>>'

I understand my problem, but I do not understand how to fix it. Could someone provide solutions or alternative suggestions?

guyus15
  • 81
  • 6
  • 2
    `EventManager::AddListener` seems rather noisy and redundant in its current state. The entirety of that function could be implemented as `s_events[T{}].push_back(listener);` – Drew Dormann Oct 13 '22 at 14:32
  • Keyword here is upcasting of pointers: cast StartEvent* to GameEvent*, then pass it around. If you need to interact with your objects, give the base class a pure virtual method that the children override. – nick Oct 13 '22 at 14:38
  • I'm also wondering what is happening in that if statement. Where is the vector `listeners` defined? Isn't it being created every time that if is executed and then dies once it goes out of scope? – Sailanarmo Oct 13 '22 at 14:41
  • @nick Would this not result in object slicing? – guyus15 Oct 13 '22 at 15:07
  • @DrewDormann Will this initialise vectors which aren't yet initialised in the map? – guyus15 Oct 13 '22 at 15:08
  • 1
    @Sailanarmo The listeners vector is just a reference to one of the vectors in the map – guyus15 Oct 13 '22 at 15:09
  • @guyus15 `s_Events[x]` will create a default-constructed value for the key `x` if there is not already an entry for that key. – Drew Dormann Oct 13 '22 at 15:19
  • @DrewDormann Okay thanks, yes that cleans up that function a lot. But I still have the issue of the event types not matching the vector template type – guyus15 Oct 13 '22 at 15:44
  • @guyus: Object slicing happens when you copy a value of the childs type into one of the parents type (among other ways). When you cast a pointer, nothing really changes for the object, so that's not a concern. There is a case for making the parent class virtual to avoid memory leaks, though: https://stackoverflow.com/a/461224/20213170 – nick Oct 14 '22 at 05:54
  • You can’t safely convert (or wrap) a function of `StartEvent` as a function of `GameEvent`—the latter would accept arguments that could not be converted to the latter (especially if by value!). (The technical term is that function types are **contravariant** in their parameter types.) – Davis Herring Oct 14 '22 at 20:25

0 Answers0