'Save template type parameter pack for later use

I'm working on a code where I can bind events and callbacks to react to those events, the interface looks like this:

void on_close();

struct S
{
    void the_app_is_closing();
};

S s;

Events::Register(app::CLOSE, on_close);
Events::Register(app::CLOSE, s, &S::the_app_is_closing);

...
...

if (/* something happens */)
    Events::Broadcast(app::CLOSE);

Internally it keeps a container which associates an enum value identifying an event with all the functions expected to react to that event. Those functions are kept into an object which can hold free functions or member functions and feeds the functions through a template function (apply) that forwards the parameters:

class callback
{
    struct base {};

    template <typename ... params_pack>
    struct callable : public base
    {
        callable(void(*a_function)(params_pack ...)) :
            m_call{a_function}
        {}

        template <typename listener_t>
        callable(listener_t &a_listener, void(listener_t:: *a_function)(params_pack ...)) :
            m_call{[&a_listener, &a_function](params_pack ... a_argument)
            {
                (a_listener.*a_function)(a_argument ...);
            }}
        {}

        std::function<void(params_pack ...)> m_call;
    };

    template <typename ... params_pack>
    auto build(void(*a_function)(params_pack ...))
    {
        return std::make_unique<callable<params_pack ...>>(a_function);
    }

    template <typename listener_t, typename ... params_pack>
    auto build(listener_t &a_listener, void(listener_t:: *a_function)(params_pack ...))
    {
        return std::make_unique<callable<params_pack ...>>(a_listener, a_function);
    }

    std::unique_ptr<base> m_function{nullptr};

public:

    template <typename function_t>
    callback(function_t a_function) :
        m_function{build(a_function)}
    {}

    template <typename listener_t, typename function_t>
    callback(listener_t &a_listener, function_t a_function) :
        m_function{build(a_listener, a_function)}
    {}

    template <typename ... params_pack>
    void apply(params_pack ... a_argument) const
    {
        if (auto &call = *static_cast<callable<params_pack ...> *>(m_function.get());
            std::is_invocable_v<decltype(call.m_call), params_pack ...>)
        {
            call.m_call(a_argument ...);
        }
    }
};

I have an important bug on that apply function that can be reproduced with this code:

void string_parameter(const std::string &s) { std::cout << s << '\n'; }
void long_parameter(long l) { std::cout << l << '\n'; }

int main()
{
    callback l(long_parameter);
    callback s(string_parameter);

    l.apply(123);
    s.apply("Test");

    return 0;
}

Even if you can call string_parameter directly with a literal string and long_parameter directly with a literal integer, doing the call through callback::apply messes everything up. I know why it is happening:

  1. I'm static_casting callback::callable<const std::string &> to callback::callable<const char *>.
  2. Then the callable::m_call which underlying type is std::function<const std::string &> thinks it is std::function<const char *>.
  3. The callable::m_call receives a literal string but is reinterpreted as std::string during the std::function call, creating the mess.
  • Same story with long and int.

The solution would be to save the parameter pack used on construction in order to use it inside apply:

template <typename function_t>
callback(function_t a_function) :
    m_function{build(a_function)}
{ PARAMETERS = function_t.parameters } // ???

template <typename listener_t, typename function_t>
callback(listener_t &a_listener, function_t a_function) :
    m_function{build(a_listener, a_function)}
{ PARAMETERS = function_t.parameters } // ???


...
...


template <typename ... params_pack>
void apply(params_pack ... a_argument) const
{
//                    Saved parameters --> vvvvvvvvvvvvvv
    if (auto &call = *static_cast<callable<PARAMETERS ...> *>(m_function.get());
        std::is_invocable_v<decltype(call.m_call), params_pack ...>)
    {
        call.m_call(a_argument ...);
    }
}

But I don't know if this is even possible. Any advise?

Thanks!



Solution 1:[1]

This is your problem:

class callback

it should be

template<class...Args>
class callback

because you have to think about what happens when the types do not match

void string_parameter(const std::string &s) { std::cout << s << '\n'; }
void long_parameter(long l) { std::cout << l << '\n'; }

callback<long> l(long_parameter);
callback<std::string> s(string_parameter);

l.apply(123);
s.apply("Test");

which works flawlessly.

Now you run into the problem of a central enum for all callbacks.

Events::Register(app::CLOSE, on_close);
Events::Register(app::CLOSE, s, &S::the_app_is_closing);

The problem is that all use of app::CLOSE must know what the signature of the callback must be. The code registering it must know, and the code invoking the callback must know.

Your design, however, carefully forgets this fact, and forces type unsafety at both ends. Then you add so,e template code in the middle to ferry types around... which even if it did work, would be work for no good reason.

template<app::event e>
void Events::Register(event_sig<e>* pf);
template<app::event e, class T>
void Events::Register(T* pt, event_mem_sig<T,e>* pf);
template<app::event e, class...Ts>
void Event::Broadcast(Ts&&....ts);

here we have a more sensible API. The event type is compile time value, so we can do type checking, and store the event callbacks in a type safe list.

...

Now, if you have a reasonably bounded number of events (ie, not 1000s of which under 1% are subscribed to), an even simpler solution is to make an event queue an actual object, instead of an enum and traits.

using token=std::shared_ptr<void>;
template<class...Args>
struct broadcaster {
  size_t broadcast(Ts...ts)const;
  token subscribe(std::function<void(Ts...)>);
  void unsafe_subscribe(void(*)(Ts...));
  // IMPLEMENTATION
};

now your code becomes

struct Events {
  broadcaster<> appClosing;
};
Events g_events;

struct S
{
  void the_app_is_closing();
  token listening;
};

S s;

s.listening=g_events.appClosing.subscribe(&s, &S::the_app_is_closing);
g_events.appClosing.unsafe_subscribe(on_close);
g_events.appClosing.broadcast();

The types of the arguments are now tied to the appClosing object, so it is checked at both sibscription and at broadcast, conversion is done automatically.

Here each broadcaster maintains its own listener queue (hence bit above about "1000s of event types most unused). Extra work can be done to reduce the queue storage and share it, but that should onlh be done if you need it. And you probably won't.

The enum solution seems like it reduces duplication, but uniform lists of things with non uniform types are often a sign your list shoudln't be uniform.

Members of a struct are a fine way to list non uniform things. Having them be generated from a template means there isn't code writing duplication. And identical signature broadcasters will share binary implementations, somit isn't inefficient.

Solution 2:[2]

tl;dr:

  • Completely abstracting away the signature of the function AND still calling it in a type-safe way is impossible in C++
  • A type-based event system could be a good alternative

1. Why it's impossible to do what you're asking for

1.1 How Type-Erasure works

Type-erasure is fundamentally based on polymorphism. By defining a set of methods that all objects we want to store have in common (the interface) we don't need to know the actual type we're dealing with.

There is no way to do type-erasure without involving polymorphism.

For example, a very crude implementation of std::function could look like this:


template<class RetVal, class... Args>
class function {
public:
    template<class U>
    function(U u) : ptr(new impl<U>(u)) {}
    ~function() { delete ptr; }

    RetVal operator()(Args... args) {
        return ptr->call(args...);
    }

private:
    struct base {
        virtual ~base() = default;
        virtual RetVal call(Args... args) = 0;
    };

    template<class T>
    struct impl : base {
        impl(T t): t(t) {}

        RetVal call(Args... args) override {
            return t(args...);
        }

    private:
        T t;
    };

    base* ptr;
};

template<class RetVal, class... Args>
class function<RetVal(Args...)> : public function<RetVal, Args...> {};

godbolt example

This is how std::function accomplishes to store any function object that is compatible with it's signature - it declares an interface (base) that will be used by all function objects (impl).

The interface only consists of 2 functions in this case:

  • The destructor (we need to know how to properly cleanup the function object)
  • The call() function (for invoking the actual function)

Sidenote 1: A real std::function implementation would need a couple more interface functions, e.g. for copying / moving the callable

Sidenote 2: Your existing implementation has a small bug: struct base MUST have a virtual destructor, otherwise the destructor of struct callable would never be called, resulting in undefined behaviour.

1.2 How your callable would need to work

What you want is an object that completely erases both the function object AND the parameters that you pass.

But what should your interface then look like?

struct base {
  virtual ~base() = default;

  virtual ??? call(???); // how should this work?
};

This is the underlying problem you're facing - it's impossible to define an interface for your callable - because you don't know what the arguments are gonna be.

This is what @Yakk - Adam Nevraumont implied with "non-uniform" objects - there is no definition of call() that can handle all potential function types.

1.3 Options

So at that point you basically have two options:

  • Don't erase the function type (like @Yakk - Adam Nevraumont suggested)
  • Sacrifice compile-time type safety and maintainability by creating an interface that can deal with arbitrary function types

The latter option is what your code currently uses - either the function parameters match or your code has undefined behaviour.

A few other ways to implement it that don't rely on undefined behaviour could be:

  • Add an interface function for each possible argument combination
    struct base {
        /* ... */
    
        // All possible ways a `callable` could potentially be invoked
        virtual void call(int val0) { throw std::exception("invalid call"); };
        virtual void call(std::string val0) { throw std::exception("invalid call"); };
        virtual void call(const char* val0) { throw std::exception("invalid call"); };
        virtual void call(int val0, std::string val1) { throw std::exception("invalid call"); };
        virtual void call(int val0, const char* val1) { throw std::exception("invalid call"); };
        // etc...
    }
    
    // then implement the ones that are sensible
    struct callable<std::string> : public base {
        /* ... */
        void call(std::string val0) override { /* ... */ }
        void call(const char* val0) override { /* ... */ }
    }
    
    This obviously gets out of hand rather quickly.
  • "Accept anything" interface
    struct base {
        /* ... */
    
        virtual void call(std::any* arr, int length);
    };
    
    // then implement the ones that are sensible
    struct callable<std::string> : public base {
        /* ... */
        void call(std::any* arr, int length) override {
            if(length != 1) throw new std::exception("invalid arg count");
            // will throw if first argument is not a std::string
            std::string& value = std::any_cast<std::string&>(arr[0]);
            /* ... */
        }
    };
    
    A bit better, but still looses compile-time type safety.

1.4 Conclusion

  • Compile-time type-safety with type-erasure is only possible if there is an uniform interface for all possible objects.
  • It is technically possible to type-erase non-uniform objects, but if you do that you'll loose compile-time type-safety (and need to do those checks at runtime instead)

2. Another Approach: Type-Based Event System

I'd like to propose a different way to handle the events that allows you to have arbitrary events without having to hard-code them into your Events class.

2.1 Basic Functionality

The main idea of this implementation is to have a class for each event you'd want to have that contains the parameters for the given event, e.g.:

struct AppClosingEvent {
    const std::string message;
    const int exitCode;
};

struct BananaPeeledEvent {
    const std::shared_ptr<Banana> banana;
    const std::shared_ptr<Person> peeler;
};

// etc...

This would then allow you to use the type of the event struct as a key for your event listeners.

A very simple implementation of this event system could look like this: (ignoring unregistration for now)

class EventBus {
private:
    using EventMap = std::multimap<std::type_index, std::function<void(void*)>>;

    // Adds an event listener for a specific event
    template<class EvtCls, class Callable>
    requires std::is_invocable_v<Callable, EvtCls&>
    inline void Register(Callable&& callable) {
        callbacks.emplace(
            typeid(EvtCls),
            [cb = std::forward<Callable>(callable)](void* evt) {
                cb(*static_cast<EvtCls*>(evt));
            }
        );
    }

    // Broadcasts the given event to all registered event listeners
    template<class EvtCls>
    inline void Broadcast(EvtCls& evt) {
        auto [first, last] = callbacks.equal_range(typeid(EvtCls));
        for(auto it = first; it != last; ++it)
            (it->second)(&evt);
    }

private:
    EventMap callbacks;
};
  • Register() takes a callable object that needs to be invocable with the given event type. Then it type-erases the callable so we can store it as a std::function<void(void*>
  • Broadcast(evt) looks up all event listeners that are registered based on the type of the event object and calls them.

Example Usage would look like this:

EventBus bus;

bus.Register<AppClosingEvent>([](AppClosingEvent& evt) {
    std::cout << "App is closing! Message: " << evt.message << std::endl;
});

bus.Register<BananaPeeledEvent>([](BananaPeeledEvent& evt) {
    // TODO: Handle banana peeling
});

AppClosingEvent evt{"Shutting down", 0};
bus.Broadcast(evt);

By using the type of the event as the key both Register() and Broadcast() are completely type-safe - it's impossible to register a function with incompatible function arguments.

Additionally the EventBus class doesn't need to know anything about the events it'll handle - adding a new event is as simple as defining a new class with the members you need for your event.

2.2 Adding the ability to unregister an event listener

I chose to use a multimap in this case because they guarantee to not invalidate iterators, unless the element the iterator points to itself gets removed from the multimap - which allows us to use a multimap iterator as the registration token for the event handler.

Full implementation: godbolt example

/*
  EventBus - allows you to register listeners for arbitrary events via `.Register()`
  and then later invoke all registered listeners for an event type with `.Broadcast()`.
  Events are passed as lvalues, to allow event handlers to interact with the event, if required.
*/
class EventBus {
private:
    using EventMap = std::multimap<std::type_index, std::function<void(void*)>>;
public:
    /*
        Represents a registered event handler on the EventBus.
        Works a lot like std::unique_ptr (it is movable but not copyable)
        Will automatically unregister the associated event handler on destruction.
        You can call `.disconnect()` to unregister the event handler manually.
    */
    class Connection {
    private:
        friend class EventBus;
        // Internal constructor used by EventBus::Register
        inline Connection(EventBus& bus, EventMap::iterator it) : bus(&bus), it(it) { }

    public:
        inline Connection() : bus(nullptr), it() {}
        // not copyable
        inline Connection(Connection const&) = delete;
        inline Connection& operator=(Connection const&) = delete;

        // but movable
        inline Connection(Connection&& other)
            : bus(other.bus), it(other.it) {
            other.detach();
        }

        inline Connection& operator=(Connection&& other) {
            if(this != &other) {
                disconnect();
                bus = other.bus;
                it = other.it;
                other.detach();
            }

            return *this;
        }

        inline ~Connection() {
            disconnect();
        }
    
        // Allows to manually unregister the associated event handler
        inline void disconnect() {
            if(bus) {
                bus->callbacks.erase(it);
                detach();
            }
        }

        // Releases the associated event handler without unregistering
        // Warning: After calling this method it becomes impossible to unregister
        //          the associated event handler.
        inline void detach() {
            bus = nullptr;
            it = {};
        }

    private:
        EventBus* bus;
        EventMap::iterator it;
    };

    // Adds an event listener for a specific event
    template<class EvtCls, class Callable>
    requires std::is_invocable_v<Callable, EvtCls&>
    inline Connection Register(Callable&& callable) {
        auto it = callbacks.emplace(
            typeid(EvtCls),
            [cb = std::forward<Callable>(callable)](void* evt) {
                cb(*static_cast<EvtCls*>(evt));
            }
        );

        return { *this, it };
    }

    // Broadcasts the given event to all registered event listeners
    template<class EvtCls>
    inline void Broadcast(EvtCls& evt) {
        auto [first, last] = callbacks.equal_range(typeid(EvtCls));
        for(auto it = first; it != last;)
            (it++)->second(&evt);
    }

private:
    EventMap callbacks;
};

With this you can easily register listeners and unregister them later (e.g. if the class they're bound to gets destructed)

Example:

struct DispenseNachosEvent {};
struct DispenseCheeseEvent {};

class NachoMachine {
public:
    NachoMachine(EventBus& bus) {
        // register using std::bind
        nachoEvent = bus.Register<DispenseNachosEvent>(
            std::bind(
                &NachoMachine::OnDispenseNachos,
                this,
                std::placeholders::_1
            )
        );

        // register with lambda
        cheeseEvent = bus.Register<DispenseCheeseEvent>(
            [&](DispenseCheeseEvent& evt) {
                OnDispenseCheese(evt);
            }
        );
    }

    // Default destructor will automatically
    // disconnect both event listeners

private:
    void OnDispenseNachos(DispenseNachosEvent&) {
        std::cout << "Dispensing Nachos..." << std::endl;
    }

    void OnDispenseCheese(DispenseCheeseEvent&) {
        std::cout << "Dispensing Cheese..." << std::endl;
    }

private:
    EventBus::Connection nachoEvent;
    EventBus::Connection cheeseEvent;
};

2.3 Other benefits

  • If you want you can also allow the event handlers to modify the event object - e.g. cancel it - which allows you to return state to the piece of code that called Broadcast() Example:
    struct CancelableExampleEvent {
        inline void Cancel() { isCancelled = true; }
        inline bool IsCancelled() { return isCancelled; }
    
        CancelableExampleEvent(std::string message) : message(message) {}
    
        const std::string message;
    private:
        bool isCancelled = false;
    };
    
    // Usage:
    CancelableExampleEvent evt;
    bus.Broadcast(evt);
    if(!evt.IsCancelled()) {
        // TODO: Do something
    }
    
  • Event Handlers can remove themselves - this is usually tricky to implement due to iterators being invalidated, but with multimaps it's rather easy to implement:
    template<class EvtCls>
    inline void Broadcast(EvtCls& evt) {
        auto [first, last] = callbacks.equal_range(typeid(EvtCls));
        for(auto it = first; it != last;)
            (it++)->second(&evt);
    }
    
    By incrementing it before calling the function we make sure that it remains valid, even if the event handler chooses to unregister itself as part of its callback. e.g. this would work:
    EventBus::Connection con;
    con = bus.Register<SomeEvent>([&con](SomeEvent&){
        std::cout << "Received event once!" << std::endl;
        con.disconnect();
    });
    

2.4 Try it online!

Here's a godbolt that contains the entire code of this post to try it out.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Yakk - Adam Nevraumont
Solution 2