-1

I originally asked a question on how place holder generators work using integer_sequence expansion: How does this placeholder generator work?

Using this I wrote the following header-only library:

/// Source: https://stackoverflow.com/questions/9568150/what-is-a-c-delegate/9568485#9568485
/// Source: https://stackoverflow.com/questions/21663012/binding-a-generic-member-function/21664270#21664270
#pragma once
#ifndef INVOKABLE_CALLBACKS
#define INVOKABLE_CALLBACKS
    #include <functional>
    #include <vector>
    #include <mutex>
    
    #pragma region Placeholder Generator
    /// This generates the expansion for vargs into the invokable templated types.
    template<int>
    struct placeholder_template {};

    namespace std {
        template<int N>
        struct std::is_placeholder<placeholder_template<N>> : std::integral_constant<int, N + 1> {};
    }
    #pragma endregion
    #pragma region Invokable Callback
    /// Function binder for dynamic callbacks.
    template<typename... A>
    class callback {
    protected:
        // Unique hash ID for class comparisons.
        size_t hash;
        // The function this callback represents.
        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.
        inline bool operator == (const callback<A...>& cb) noexcept { return (hash == cb.hash); }
        /// Compares not-equality of callbacks.
        inline bool operator != (const callback<A...>& cb) noexcept { return (hash != cb.hash); }
        /// Executes this callback with templated arguments.
        inline callback<A...>& operator () (A... args) noexcept { func(args...); return (*this); }
        /// Copy constructor.
        inline callback<A...>& operator = (const callback<A...>& cb) { return clone(cb); }

        /// 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) ^ reinterpret_cast<size_t>(obj) ^ (&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; }

        /// Clones this callback function.
        callback<A...>& clone(const callback<A...>& cb) {
            func = cb.func;
            hash = cb.hash;
            return (*this);
        }
    };
    #pragma endregion
    #pragma region Invokable Event
    /// Thread-safe event handler for callbacks.
    template<typename... A>
    class invokable {
    protected:
        /// 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.
        inline invokable<A...>& operator += (const callback<A...>& cb) noexcept { return hook(cb); }
        /// Removes a callback from this event.
        inline invokable<A...>& operator -= (const callback<A...>& cb) noexcept { return unhook(cb); }
        /// Removes all registered callbacks and adds a new callback.
        inline invokable<A...>& operator = (const callback<A...>& cb) noexcept { return rehook(cb); }
        /// Execute all registered callbacks.
        inline invokable<A...>& operator () (A... args) noexcept { return invoke(args...); }
        // Copies all of the callbacks from the passed invokable object into this invokable object.
        inline invokable<A...>& operator = (const invokable<A...>&& other) noexcept { return clone(other); }

        /// 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...> i) { return cb == i; });
            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);
        }

        /// Copies all of the callbacks from the passed invokable object into this invokable object.
        invokable<A...>& clone(const invokable<A...>& invk) {
            if (this != &invk) callbacks.assign(invk.callbacks.begin(), invk.callbacks.end());
            return (*this);
        }
    };
    #pragma endregion
#endif

However anytime this library is used, the following warnings are given and rightly so by the compiler: (error about runtime stack overflow, even though this is compile-time templated)

 warning C4717: 'std::callback<VkCommandBuffer_T *>::create<std::callback<VkCommandBuffer_T *> &,0>': recursive on all control paths, function will cause runtime stack overflow

Example Usage:

#include <iostream>
#include "invokable.hpp"

class myclass {
private:
    int id = 0;
public:
    myclass(int id) {
        this->id = id;
    };

    void function(int x) {
        std::cout << (id * x) << std::endl;
    };
};

class someclass {
public:
    void func(int x) {
        std::cout << (x / 6) << std::endl;
    };
};

int main() {
    invokable<int> myevent;
    myclass x(2), y(1);
    someclass z;
    callback<int> callx(&x, &myclass::function);
    callback<int> cally(&y, &myclass::function);
    callback<int> callz(&z, &someclass::func);

    myevent += callx;
    myevent += cally;
    myevent += callz;
    myevent(12);
};

/*
    Output:
        24
        12
        2
*/

EDIT: Also this is strange. If I create a new console app in VS2022 CommunityPREV and add the lbirary/example, it does actually give a stackoverflow error. However, this does not happen in any other case.

FatalSleep
  • 307
  • 1
  • 2
  • 15
  • 3
    Please provide a [mre] with emphasis on _minimal_. – Passer By Mar 19 '23 at 05:22
  • I think you should remove `placeholder_template`, it doesn't seem useful and you apparently misuse it. The `make_integer_sequence` is also out of place. Just read documentation of `std::bind` – ALX23z Mar 19 '23 at 07:03
  • It's better now you have a compilable example, but there are still a lot of code unrelated to your question in the first snippet, e.g. `clone` and whatnot. – Passer By Mar 19 '23 at 07:55
  • @PasserBy the first snippet is the entire library, that's why. The specific problem only happens on instantiation of callback() where the integer_sequence is being built. – FatalSleep Mar 19 '23 at 09:47
  • @ALX23z I can't remove any of it, unless there's a better solution. This is necessary to build user-templates at compile time because the library doesn't know what the user wants. – FatalSleep Mar 19 '23 at 09:49
  • Too many problems of various sources. You did violate rule of 0/3/5, which will lead in spurious wierd bugs. You are overly complicating the design of `callback`. And you have missed the mechanism to ensure lifetime correctness of bound objects. Start practicing with simpler programs, so that you can get clear answers for each problem in isolation. This code needs a few pages of criticism to show you all sorts of problems you've got. Please slow down a bit, and start with simpler challenges. – Red.Wave Mar 19 '23 at 09:53
  • 1
    @FatalSleep the better solution is removing all the redundant uses of the integer sequence and the placeholders. Just write `this->func=std::function(f)` and `this->func=std::function(std::bind(f, obj));` instead. Also don't use `func` for both member variable and function input simultaneously - that's a bad practice. – ALX23z Mar 19 '23 at 09:56
  • Also, you should write `std::forward<_Fx>(f)` instead of `f` for perfect forwarding. – ALX23z Mar 19 '23 at 09:59
  • I know its the entire library. That's why I'm asking you to remove the unrelated stuff. Remove _anything and everything_ that is not necessary to reproduce the error. – Passer By Mar 19 '23 at 10:02
  • @ALX23z thank you for the recommendation, I'll do a rewrite and get back to you. – FatalSleep Mar 20 '23 at 05:47
  • @ALX23z also you can't bind a function with variable number of arguments with `std::bind` without argument expansion. – FatalSleep Mar 20 '23 at 06:22
  • @Red.Wave the design isn't overly complicated. If I want to have variadic template binding for arguments you need to either use placeholder expansion for `std::bind` or manually select a binding based on the number of arguments based (which is like 20 derivatives). – FatalSleep Mar 20 '23 at 06:29
  • @FatalSleep then just replace it via lambda call. [f,obj](auto&&... args){std::invoke(f,obj,args...);}; – ALX23z Mar 20 '23 at 07:18
  • Or use `std::bind_front` if you are already using c++20. – ALX23z Mar 20 '23 at 07:24
  • @ALX23z okay awesome. `std::bind_front` doesn't work, same issue as above. However the lambda call does work which makes sense because of the direct argument expansion (instead of _Types&&... Args like std::bind/_front). – FatalSleep Mar 20 '23 at 08:17
  • @FatalSleep, The OP doesn't seem to be baked enough to pull this off at once. Did you notice the RAII related bug and lifetime issues with bound objects in the 1st place? Use of `std::placeholders` with this level of experience is an overcomplication. This code has a long way to be totally fixed, due to multiple issues. – Red.Wave Mar 20 '23 at 11:02
  • @Red.Wave do you lack tack or...? Yes I realized the issues, but couldn't find a viable solution until ALX23z mentioned. I am doing a full rework which solves the presented issues. Will post changes when I'm done. – FatalSleep Mar 21 '23 at 06:36
  • @Red.Wave also if you noticed the github link this was written 2yrs ago and just used as is. Hence the question now. – FatalSleep Mar 21 '23 at 06:40
  • @FatalSleep, As I mentioned earlier, you are not still ready for this kind of stuff. I guess you have a strong background in C#, but that's not C++. You have more fundamental problems in this snippet. Your `invokable` has double-delete problem, because of your unfamiliarity with practical implementation of RAII(Rule of 0/3/5). Next, you don't have any solution for dangling object pointers stored in your `invokable` instances. A little bit of problem with varidic templates is the last bit of concern to me. – Red.Wave Mar 21 '23 at 12:17
  • 1
    @FatalSleep, If this project had been anything more than a personal exercise, you should've known about QT signal-slot mechnism or boost.signsls2 as famous existing practice; Then you should've been able to address their shortcomings, along with your alternate approach. So long as this code is an exercise, you need to practice and master some basic concepts. There are a lot misfit methods of learning C++ everywhere. I won't be surprised if it takes more than 7 years for a programmer to find the right track. And I like to see you on that track. – Red.Wave Mar 21 '23 at 12:25

1 Answers1

0

Given @ALX23z and @Red.Wave recommendations for std::invoke and RAII compliance I have rewritten the library. A couple of notes: I initially had trouble with std::invoke not being able to pin-point an appropriate overload when called due to lack of RAII compliance when used with std::vector. My custom constructors got in the way of the compiler properly interpreting the constructors and templates.

The appropriate method was to ONLY provide callback<A...> with operator overloads for == and != for the hash comparison (to avoid hooking the same function twice) and providing only the two constructors for the required static/lambda/member function inputs.

After this it was relatively clean and easy to reimplement invokable<A...> with the same RAII compliant approach. For clarity, all callback invoke operators that overloaded operator () were removed.

The hash function itself was probably the biggest problem, never providing good or unique hashes and resulting in all hashes being the same. std::function exposes its own hash_code() method, so I defaulted to using this, then XOR'ing it with std::hash<T*>{}(obj) for a basic combined hash with the class instance bound to a callback in the case of non-static callback methods. This provides unique hashes for each function or function/instance pair, but NOT for callback<A...> which is intentional--to avoid duplicate hooks on invokable<A..>.

/// 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>

    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); }
    };

    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

Overall a more stable implementation without mangled compiler errors in the case of trying to hook callbacks to invokables with mismatched parameters.

FatalSleep
  • 307
  • 1
  • 2
  • 15