Search code examples
c++templatesc++20functor

How can I handle void return type in a templated functor in C++20?


I have built a templated functor object which I can use to manage lambdas which need to recurse and survive across scopes. It's not very pretty (it uses void pointers and an std::function instance), but it works for the use-cases I need it to. (I'd appreciate if posters could withhold comments about how it's not type-safe and terribly bad practice. I'm aware.)

There is a glaring issue with it, however: it can't handle lambdas which return void, because some of the paths try to store return values in a variable. I need to know how to use if constexpr statements to detect if the result of the functor's lambda is void, and handle it appropriately. This isn't a unique problem, but all the results I've found are extremely out of date, many of them using the now-depreciated result_of_t.

Any help would be greatly appreciated.


#include <iostream>
#include <string>
#include <functional>

#define uint unsigned int

//! A standardised wrapper for lambda functions, which can be stored in pointers, used recursively, keep track of external storage via a void *, and set to self destruct when no longer useful.
template <class F, bool UsesDataStorage>
class Functor {
protected:
    std::function<F> m_f; //!< The lambda stored by the wrapper
    void* m_data = nullptr; //!< A void pointer which will be given to the lambda if `UsesDataStorage`. Note that cleanup is delegated to the lambda; the functor instance will not handle it.
    bool m_selfDestructing = true; //!< Whether the combinator will self-destruct should its lambda mark itself as no longer useful.
    bool m_selfDestructTrigger = false; //!< Whether the combinator's lambda has marked itself as no longer useful.
public:
    inline bool usesDataStorage() const { return UsesDataStorage; } //!< Return whether this functor is set up to give its function a `data` void-pointer, which will presumably be set to a data-structure.
    inline void* getData() const { return m_data; } //!< Returns the void pointer which is passed to the lambda at each call (if the functor instance uses data storage).
    inline void setData(void* data) { m_data = data; }  //!< Sets the void pointer which is passed to the lambda at each call (if the functor instance uses data storage).
    inline bool canSelfDestruct() const { return m_selfDestructing; } //!< Returns whether the LambdaWrapper will delete itself when instructed to by the contained lambda.
    inline void triggerSelfDestruct() { m_selfDestructTrigger = true; } //!< Triggers wrapper self-deletion at the end of ruinning the lambda.

    Functor(const std::function<F>& f, bool canSelfDestruct = true) :
        m_f(f),
        m_selfDestructing(canSelfDestruct)
    {} //!< Constructor for Functor instances which DON'T use data storage. Note that the given function should always take a void pointer as the first argument, which is where a pointer to the Functor instance will be passed.
    Functor(std::function<F>&& f, bool canSelfDestruct = true) :
        m_f(f),
        m_selfDestructing(canSelfDestruct)
    {} //!< Constructor for Functor instances which DON'T use data storage. Note that the given function should always take a void pointer as the first argument, which is where a pointer to the Functor instance will be passed.
    Functor(const std::function<F>& f, void* data, bool canSelfDestruct = true) :
        m_f(f),
        m_data(data),
        m_selfDestructing(canSelfDestruct)
    {} //!< Constructor for Functor instances which DO use data storage. Note that the given function should always take a void pointer as the first argument, which is where a pointer to the Functor instance will be passed, and a void * for the second argument, which is where the data storage pointer is passed.
    Functor(std::function<F>&& f, void* data, bool canSelfDestruct = true) :
        m_f(f),
        m_data(data),
        m_selfDestructing(canSelfDestruct)
    {} //!< Constructor for Functor instances which DO use data storage. Note that the given function should always take a void pointer as the first argument, which is where a pointer to the Functor instance will be passed, which is where the data storage pointer is passed.

    template <typename... Args>
    decltype(auto) operator()(Args&&... args) {
        // Avoid storing return if we can, 
        if (!m_selfDestructing) {
            if constexpr (UsesDataStorage) {
                // Pass itself to m_f, then the data storage, then the arguments.
                // This should work even if the return type is void, as far as I can tell.
                return m_f(this, m_data, std::forward<Args>(args)...);
            }
            else {
                // Pass itself to m_f, then the arguments.
                // This should work even if the return type is void, as far as I can tell.
                return m_f(this, std::forward<Args>(args)...);
            }
        }
        else {
            if constexpr (UsesDataStorage) {
                // Pass itself to m_f, then the data storage, then the arguments.

                // ----- !!! -----
                // The following if constexpr statement is what I can't work out how to do.
                // ----- !!! -----
                if constexpr (std::is_same<std::invoke_result_t<std::function<F>>, void>) {
                    m_f(this, m_data, std::forward<Args>(args)...);
                    // self-destruct if necessary, allowing lamdas to delete themselves if they know they're no longer useful.
                    if (m_selfDestructTrigger) { delete this; }
                    return;
                }
                else {
                    auto r = m_f(this, m_data, std::forward<Args>(args)...);
                    // self-destruct if necessary, allowing lamdas to delete themselves if they know they're no longer useful.
                    if (m_selfDestructTrigger) { delete this; }
                    return r;
                }
            }
            else {
                // Pass itself to m_f, then the arguments.

                // ----- !!! -----
                // The following if constexpr statement is what I can't work out how to do.
                // ----- !!! -----
                if constexpr (std::is_same<std::invoke_result_t<std::function<F>>, void>) {
                    m_f(this, std::forward<Args>(args)...);
                    // self-destruct if necessary, allowing lamdas to delete themselves if they know they're no longer useful.
                    if (m_selfDestructTrigger) { delete this; }
                    return;
                }
                else {
                    auto r = m_f(this, std::forward<Args>(args)...);
                    // self-destruct if necessary, allowing lamdas to delete themselves if they know they're no longer useful.
                    if (m_selfDestructTrigger) { delete this; }
                    return r;
                }
            }
        }
    }
};
template <class F> Functor(std::function<F>, bool)->Functor<F, false>;
template <class F> Functor(std::function<F>, void*, bool)->Functor<F, true>;

int main() {
    Functor f1 = Functor(std::function([](void* self, uint val1) -> uint {
        std::cout << "f1(" << val1 << ") was called." << std::endl;
        return 2u * val1;
    }), false);
    Functor f2 = Functor(std::function([](void* self, uint val1) -> void {
        std::cout << "f2(" << val1 << ") was called." << std::endl;
        return;
    }), false);

    auto x = f1(3u); // Compiles and works.
    f2(3u); // Doesn't compile.
}

Solution

  • The line I'm looking for is this:

    if constexpr (std::is_same<std::function<F>::result_type, void>::value) {}
    

    Many thanks to @NathanOliver!