c++function-pointersmemberstd-function

Setting a "function pointer"-like member to a noexcept lambda


I cannot achieve the effect in title, as implemented in the following snippet:

#include <functional>
#include <iostream>

#define USEFUNPTR
#define USESTDFUN

class Dummy {
   public:
#ifdef USEFUNPTR
    void (*Foo)() noexcept = nullptr;
#endif
#ifdef USESTDFUN
    std::function<void() noexcept> Bar;
#endif
    void InitFuns() {
#ifdef USEFUNPTR
        Foo = []() noexcept { std::cout << "Foo\n" << std::endl; };
#endif
#ifdef USESTDFUN
        Bar = []() noexcept { std::cout << "Bar\n" << std::endl; };
#endif
    };

    ~Dummy() noexcept = default;
};};

Live
with USEFUNPTR defined:
In C++14, msvc refuses the function pointer version:

error C2440: '=': cannot convert from 'Dummy::InitFuns::<lambda_b40e7171393910f4bba39b7be19bf362>' to 'void (__cdecl *)(void) noexcept'

with USESTDFUN defined:
In C++17 gcc, clang and msvc reject the std::function version:
gcc:

error: field 'Bar' has incomplete type 'std::function<void() noexcept>' 13 | std::function<void() noexcept> Bar; msvc: error C2338: static_assert failed: 'std::function does not accept noexcept function types as template arguments.'

msvc is more explicit and indicates that, from C++17, std::function seems unable to wrap a noexcept function (there is indeed no prototype of std::function with noexcept and this specifier is part of the type from C++17, if I understood correctly (from some other SO post):

error C2440: '=': cannot convert from 'Dummy::InitFuns::<lambda_b40e7171393910f4bba39b7be19bf362>' to 'void (__cdecl *)(void) noexcept'

How can I achieve this functionality in a way that works in all C++>=14, and with all compilers?
(In an actual usecase, the InitFuns may have runtime and/or compile time parameters and the lambda definition will depend on these parameters, yet the code that will call Foo or Bar is unaware of these parameters).

NB I found several posts indicating that a lambda cannot decay to a function pointer member but I didn't understand why and if they were a workaround to achieve this functionnality.


Solution

  • There is a kind of workaround, that lets you detect if the lambda is noexcept before you assign it to a std::function<void()>. Another option is to wrap a try/catch block around any noexcept function (runtime detections, so suboptimal)

    Example here :

    #include <functional>
    #include <type_traits>
    #include <iostream>
    #include <stdexcept>
    
    // noexcept will not be picked up by std::function
    // but with SFINAE you can detect the noexcept
    
    using function_ptr_t = void (*)() noexcept;
    
    namespace traits
    {
        template<typename fn_t>
        using is_noexcept_fn = std::enable_if_t<noexcept(std::declval<fn_t>()()), fn_t>;
    
        template<typename fn_t>
        static constexpr bool is_noexcept_fn_ptr()
        {
            return std::is_convertible_v<fn_t, function_ptr_t> && noexcept(std::declval<fn_t>());
        }
    
        template<typename fn_t>
        using is_noexcept_fn_ptr_t = std::enable_if_t<is_noexcept_fn_ptr<fn_t>(), fn_t>;
    }
    
    class Dummy
    {
    public:
        // Only accept the lambda (function object) if it has a noexcept operator()
        template<typename fn_t, typename enable_t = traits::is_noexcept_fn_ptr_t<fn_t>>
        explicit Dummy(fn_t fn) :
            m_fn{ fn }
        {
        }
    
        void operator()() noexcept
        {
            m_fn();
        }
    
    private:
        std::function<void()> m_fn;
    };
    
    class Dummy2
    {
    public:
        // other option is to actually catch all exceptions for 
        // functions that are not noexcept
        template<typename fn_t>
        explicit Dummy2(fn_t fn)
        {
            if constexpr (noexcept(fn()))
            {
                m_fn = fn;
            }
            else
            {
                auto lambda = [=]()
                {
                    try
                    {
                        fn();
                    }
                    catch (...)
                    {
                        std::cout << "unhandled exeption caught\n";
                    }
                };
    
                m_fn = lambda;
            }
        }
    
        void operator()() noexcept
        {
            m_fn();
        }
    
    private:
        std::function<void()> m_fn;
    };
    
    class Dummy3
    {
    public:
        // Only accept a function pointer if it is a noexcept operator()
        template<typename fn_t, typename enable_t = traits::is_noexcept_fn_ptr_t<fn_t>>
        explicit Dummy3(fn_t fn) :
            m_fn{ fn }
        {
        }
    
        void operator()() noexcept
        {
            m_fn();
        }
    
    private:
        function_ptr_t m_fn;
    };
    
    
    void test() noexcept
    {
        std::cout << "free function\n";
    };
    
    int main()
    {
        //Dummy d1{ [] { std::cout << "does not compile\n"; } };
        Dummy d{ []() noexcept { std::cout << "does compile\n"; } };
        d();
    
        Dummy2 d2{ [] { throw std::runtime_error{"oops"}; } };
        d2();
    
        Dummy3 d3_lambda{ []() noexcept { std::cout << "using lmabda's function pointer\n"; } };
        d3_lambda();
    
        Dummy3 d3_free_function(test);
        d3_free_function();
    
        // indeed fails to compile
        //Dummy3 d3_lambda_fail{ [] { std::cout << "using lmabda's function pointer\n"; } };
        //d3_lambda();
    
        return 0;
    }