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

115 Views Asked by At

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.

1

There are 1 best solutions below

14
Pepijn Kramer On BEST ANSWER

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