2
\$\begingroup\$

This is a small library I wrote to make event callbacks with variadic templates work like C#'s event system (placeholder template by "dyp" at http://stackoverflow.com/a/21664270).

Just like in C# you can subscribe/unsubscribe/call multiple function callbacks to a single event (invokable class in my library) using the =, +=, -= and () operators. With the exception of unhook_all() by itself which doesn't fit in with operators very well unless I add an operator for = for comparison to another invokable instance and check for null, which would keep you from ever setting an invokable class to null.

Everything works from what I've tested... My only concern is thread-safety and the hash for comparison.

Creating an invokable event with input parameters:

// Without parameters
invokable<> event;
// OR
// With parameters (int, int)
invokable<int, int> event;

class myclass {
public:
    void F(int x, int y) {...};
};

myclass inst;
callback<int, int> call(&inst, &myclass::F);

Hooking and unhooking a callback to/from an event:

// Hoook
event += call;
// OR
event.hook(call);

// Unhook
event -= call;
// OR
event.unhook(call);

// Hook Event & Unhook All Other Hooked events
event = call;
// OR
event.hook_unhook(call);

Unhooking all hooked events:

event.unhook_all();

Invoking an event--notifies subscribed callbacks:

event(10, 10);
// OR
event.invoke(10, 10);
Lambda & Static Class Methods:

class myclass {
public:
    static void function() {
       cout << "Static Call" << endl;
    }
};

invokable<> event;
callback<> static_call(&myclass::function);
callback<> lambda_call([](){cout << "Lambda Call" << endl;});
...
#pragma once
#ifndef INVOKABLE_EVENTS
#define INVOKABLE_EVENTS
    #include <functional>
    #include <type_traits>
    #include <vector>
    #include <algorithm>
    #include <mutex>
    
    /// by "dyp" at http://stackoverflow.com/a/21664270
    /// This generates the expansion for vargs into the invokable templated types.
    #pragma region Placeholder Generator
    template<int>
    struct placeholder_template {};

    namespace std {
        template<int N>
        struct is_placeholder<placeholder_template<N>> : integral_constant<int, N + 1> {};
    };
    #pragma endregion

    #pragma region Invokable Callback
    /// Function binder for dynamic callbacks.
    template<typename... A>
    class callback {
    private:
        // Unique hash ID.
        size_t hash;
        // Bound function.
        std::function<void(A...)> func;
        
        // Creates and binds the function callback with a class instance for member functions.
        template<typename T, class _Fx, std::size_t... Is>
        void create(T* obj, _Fx&& func, std::integer_sequence<std::size_t, Is...>) {
            this->func = std::function<void(A...)>(std::bind(func, obj, placeholder_template<Is>()...));
        };

        // Creates and binds the function callback for a static/lambda/global function.
        template<class _Fx, std::size_t... Is>
        void create(_Fx&& func, std::integer_sequence<std::size_t, Is...>) {
            this->func = std::function<void(A...)>(std::bind(func, placeholder_template<Is>()...));
        };

    public:
        /// Compares equality of callbacks.
        const inline bool operator == (const callback& cb) { return (hash == cb.hash); };
        
        /// Compares not-equality of callbacks.
        const inline bool operator != (const callback& cb) { return (hash != cb.hash); };
        
        /// Executes this callback with arguments.
        const inline callback& operator () (A... args) { func(args...); return (*this); };
        
        /// Construct a callback with a template T reference for instance/member functions.
        template<typename T, class _Fx>
        callback(T* obj, _Fx&& func) {
            hash = reinterpret_cast<size_t>(&this->func) ^ (&typeid(callback<A...>))->hash_code();
            create(obj, func, std::make_integer_sequence<std::size_t, sizeof...(A)> {});
        };

        /// Construct a callback with template _Fx for static/lambda/global functions.
        template<class _Fx>
        callback(_Fx&& func) {
            hash = reinterpret_cast<size_t>(&this->func) ^ (&typeid(callback<A...>))->hash_code();
            create(func, std::make_integer_sequence<std::size_t, sizeof...(A)> {});
        };
        
        /// Executes this callback with arguments.
        callback& invoke(A... args) { func(args...); return (*this); };

        /// Returns this callbacks hash code.
        constexpr size_t hash_code() const throw() {
            return hash;
        };
    };
    #pragma endregion

    #pragma region Invokable Event
    /// Thread-safe event handler for callbacks.
    template<typename... A>
    class invokable {
    private:
        /// Shared resource to manage multi-thread resource locking.
        std::mutex safety_lock;
        /// Vector list of function callbacks associated with this invokable event.
        std::vector<callback<A...>> callbacks;

    public:
        /// Adds a callback to this event.
        const inline invokable& operator += (const callback<A...>& cb) {
            if (cb != nullptr) hook(cb);
            return (*this);
        };
        
        /// Removes a callback from this event.
        const inline invokable& operator -= (const callback<A...>& cb) { unhook(cb); return (*this); };
        
        /// Removes all registered callbacks and adds a new callback, unless defaulted then all callbacks are unhooked.
        const inline invokable& operator = (const callback<A...>& cb) { hook_unhook(cb); return (*this); };
        
        /// Execute all registered callbacks.
        const inline invokable& operator () (A... args) { invoke(args...); return (*this); };
        
        /// Adds a callback to this event, operator +=
        invokable& hook(callback<A...> cb) {
            std::lock_guard<std::mutex> g(safety_lock);

            if (std::find(callbacks.begin(), callbacks.end(), cb) == callbacks.end())
                callbacks.push_back(cb);

            return (*this);
        };
        
        /// Removes a callback from this event, operator -=
        invokable& unhook(callback<A...> cb) {
            std::lock_guard<std::mutex> g(safety_lock);

            typename std::vector<callback<A...>>::iterator it;
            it = std::find(callbacks.begin(), callbacks.end(), cb);

            if (it != callbacks.end())
                callbacks.erase(it);

            return (*this);
        };
        
        /// Removes all registered callbacks and adds a new callback, operator =
        invokable& hook_unhook(callback<A...> cb) {
            std::lock_guard<std::mutex> g(safety_lock);

            callbacks.clear();
            (*this) += cb;

            return (*this);
        };
        
        /// Removes all registered callbacks.
        invokable& unhook_all() {
            std::lock_guard<std::mutex> g(safety_lock);

            callbacks.clear();

            return (*this);
        };
        
        /// Execute all registered callbacks, operator ()
        invokable& invoke(A... args) {
            std::lock_guard<std::mutex> g(safety_lock);

            for (size_t i = 0; i < callbacks.size(); i++)
                callbacks[i](args...);

            return (*this);
        };
    };
    #pragma endregion
#endif
```
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Turn on and fix compiler warnings

Compiling your code with strict compiler warnings turned on showed some issues:

  • Stray semicolons after namespaces and function definitions.
  • const return by value from operator==() and operator!=(). The const doesn't do anything here, just remove it.

Use more auto and range-for

There are a few places where you could use auto and range-for to simplify the code. For example, in unhook() you can write:

auto it = std::find(callbacks.begin(), callbacks.end(), cb);

And in invoke() you can write:

for (auto& callback: callbacks)
    callback(args...);

Unsafe callback comparison

The way you try to make callbacks comparable is wrong. First of all, you try to create a hash of the callback, and compare hashes. However, there is no guarantee that two distinct callbacks will have two distinct hash values. Even if you think it is very unlikely to happen, don't rely on probability.

The other issue is that it is not defined by the standard what the layout of class std::function is. You assume that it stores just a pointer to some function, and maybe that assumption works on your platform, but it is not portable.

The best you can do in the constructor of callback is to take the address of func (the parameter, not this->func), and store that unhashed. However, there is a reason why you cannot equality-compare two std::function objects, and I think any attempt to make them properly comparable is doomed.

Another option would be to not bother comparing functions, but to generate a unique ID for each callback, and return that ID to the caller. They can then pass it to the unhook() function, and look up the right entry in the vector callbacks using that ID.

\$\endgroup\$
1
  • \$\begingroup\$ I've made all of these changes thank you. Small correction though, I am actually not using std::function for my hash definition. The hash definition is done in the constructor on the function reference, then the constructor calls create which sets the std::function callback afterwards. \$\endgroup\$
    – FatalSleep
    May 29, 2022 at 13:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.