Search code examples
c++functionoverloadingstd-invoke

Custom callback function cannot take overloads


I have the following class for building callback functions to any subset of function type, be it static function, lambda, lambda w/ capture list, static class functions or member class functions. Problem is that it doesn't take overloads because there's no parameter resolution when passing functions to the templated constructor(s):

template<typename... A>
class callback {
protected:
    /// Unique identifying hash code.
    size_t hash;
    /// The function bound to this callback.
    std::function<void(A...)> bound;

public:
    /// Binds a static or lambda function.
    template<class Fx>
    void bind_callback(Fx func) {
        bound = [func = std::move(func)](A... args) { std::invoke(func, args...); };
        hash = bound.target_type().hash_code();
    }

    /// Binds the a class function attached to an instance of that class.
    template<typename T, class Fx>
    void bind_callback(T* obj, Fx func) {
        bound = [obj, func = std::move(func)](A... args) { std::invoke(func, obj, args...); };
        hash = std::hash<T*>{}(obj) ^ bound.target_type().hash_code();
    }

    /// Create a callback to a static or lambda function.
    template<typename T, class Fx> callback(T* obj, Fx func) { bind_callback(obj, func); }

    /// Create a callback to a class function attached to an instance of that class.
    template<class Fx> callback(Fx func) { bind_callback(func); }

    /// Compares the underlying hash_code of the callback function(s).
    bool operator == (const callback<A...>& cb) { return hash == cb.hash; }

    /// Inequality Compares the underlying hash_code of the callback function(s).
    bool operator != (const callback<A...>& cb) { return hash != cb.hash; }

    /// Returns the unique hash code for this callback function.
    constexpr size_t hash_code() const throw() { return hash; }

    /// Invoke this callback with required arguments.
    callback<A...>& invoke(A... args) { bound(args...); return (*this); }
};

Usage: (fails on overloads, remove overloads and it compiles)

myclass {
public:
    void function(float x) {}
    void function(int x) {}
    static inline void static_function(float x) {}
    static inline void static_function(int x) {}
}

static inline void function(float x) {}
static inline void function(int x) {}

int main() {
    myclass inst;
    callback<int> mycallback(&inst, &myclass::function);
    callback<int> mycallback(&function);
    callback<int> mycallback(&myclass::static_function);
}

Solution

  • With the help of someone on the Together C & C++ discord, the recommendation of using std::type_identity_t in C++20 can't help solve the problem for static class members because it can't deduce the type between the function parameters and the class: template<typename T> callback(std::type_identity_t<void (T::*)(A...)> func).

    In the end the only solution is to completely ignore handling these cases and just accept std::function forcing the end-user to resolve conflicts with the function definitions on their own, e.g. using a lambda:

    /// Source: https://stackoverflow.com/questions/9568150/what-is-a-c-delegate/9568485#9568485
    /// Source: https://en.cppreference.com/w/cpp/utility/functional/invoke
    #pragma once
    #ifndef INVOKABLE_CALLBACKS
    #define INVOKABLE_CALLBACKS
        #include <functional>
        #include <vector>
        #include <mutex>
        #include <utility>
        #include <type_traits>
    
        template<typename... A>
        class callback {
        protected:
            /// Unique identifying hash code.
            size_t hash;
            /// The function bound to this callback.
            std::function<void(A...)> bound;
    
        public:
            callback(std::function<void(A...)> func) {
                bound = [func = std::move(func)](A... args) { std::invoke(func, args...); };
                hash = bound.target_type().hash_code();
            }
    
            /// Compares the underlying hash_code of the callback function(s).
            bool operator == (const callback<A...>& cb) { return hash == cb.hash; }
            
            /// Inequality Compares the underlying hash_code of the callback function(s).
            bool operator != (const callback<A...>& cb) { return hash != cb.hash; }
            
            /// Returns the unique hash code for this callback function.
            constexpr size_t hash_code() const throw() { return hash; }
            
            /// Invoke this callback with required arguments.
            callback<A...>& invoke(A... args) { bound(args...); return (*this); }
    
            /// Operator() invoke this callback with required arguments.
            void operator()(A... args) { bound(args...); }
        };
    
        template<typename... A>
        class invokable {
        protected:
            /// Resource lock for thread-safe accessibility.
            std::mutex safety_lock;
            /// Record of stored callbacks to invoke.
            std::vector<callback<A...>> callbacks;
    
        public:
            /// Adds a callback to this event, operator +=
            invokable<A...>& hook(const 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<A...>& unhook(const callback<A...> cb) {
                std::lock_guard<std::mutex> g(safety_lock);
                std::erase_if(callbacks, [cb](callback<A...> c){ return cb.hash_code() == c.hash_code(); });
                return (*this);
            }
    
            /// Removes all registered callbacks and adds a new callback, operator =
            invokable<A...>& rehook(const callback<A...> cb) {
                std::lock_guard<std::mutex> g(safety_lock);
                callbacks.clear();
                callbacks.push_back(cb);
                return (*this);
            }
    
            /// Removes all registered callbacks.
            invokable<A...>& empty() {
                std::lock_guard<std::mutex> g(safety_lock);
                callbacks.clear();
                return (*this);
            }
    
            /// Execute all registered callbacks, operator ()
            invokable<A...>& invoke(A... args) {
                std::lock_guard<std::mutex> g(safety_lock);
                for(callback<A...> cb : callbacks) cb.invoke(args...);
                return (*this);
            }
        };
    
    #endif
    

    This removes the original bind_callback() and callback constructors in favor of just accepting std::function<void(A...)> or just accepting a basic function callback.

    Which means that you'd need to use a lambda to resolve overloads, member functions, etc.

    class myclass {
    public:
        void function(float xx) { ... }
        void function(int xx) { ... }
    }
    
    myclass myinst;
    callback<float> mycallback([&myinst](int xx){ myinst.function(xx); });