Search code examples
c++templateslambda

Non-capturing lambda won't degrade to function pointer


Here is a typical C function that takes a classic function pointer as an argument:

int DoThingy( const char* stuff, int(*helper)(int))
{
    int result = 0;
    //...call helper...
    return result;
}

Below I call the above with a non-capturing lambda that magically "degrades" into a function pointer. At "A" the conversion is implicit. At "B" it's explicit. All good.

void UseThingy()
{
    auto lam = [](int)->int {
        return 42;
    };

    int i;
    i = DoThingy( "hello", lam); //"A" works

    int (*ptr)(int) = lam;
    i = DoThingy( "hello", ptr); //"B" works
}

But in this new example, the signature of the callback function depends on a template type:

template <typename T>
int DoThingy2( const char* stuff, int (*helper)(T))
{
    int result = 0;
    //...call helper...
    return result;
}

When I try to use this version as above, line "C" won't even compile. Yet the explicit version at "D" works. What? Why aren't these the same? Notice when I give an explicit template parameter at "E" it works, but certainly the <int> can be inferred from the the signature of lam, right?

void UseThingy2()
{
    auto lam = [](int)->int {
        return 42;
    };
    
    int i;
    
    i = DoThingy2( "hello", lam); //"C" won't compile

    int (*ptr)(int) = lam;
    i = DoThingy2( "hello", ptr); //"D" works

    i = DoThingy2<int>( "hello", lam); //"E" works
}

The following also doesn't compile and I wish it would:

    i = DoThingy2( "hello", [](int)->int {return 42;}); //"F" won't compile

What I should have said earlier is I need the function parameter to default to 0 so that an internal version of helper is used when none is provided by the caller.

Note this question is distinctly different from the proposed duplicate. In particular with regards to optional arguments and further it has a better set of answers using C++17 template selection features.


Solution

  • Your issue is with template argument deduction. When that process runs, no conversions happen, the compiler takes the provided object, gets the type of it, and then tries to deduce the template parameters from this type

    In this case the function gets lamba_object_from_main (made up type name), when it expects a function pointer. It can't deduce T because lamba_object_from_main is the only thing it has to work with.

    Instead of using a function pointer you can change the function to just take in anything for the second parameter like

    template <typename Func>
    int DoThingy( const char* stuff, Func helper)
    {
        int result = 0;
        //...call helper...
        return result;
    }
    

    and if you want to make sure that helper has a specific return type and parameters you can constrain the template like

    template <typename Func, std::enable_if_t<is_invocable_r_v<int, Func, int>, bool> = true>
    int DoThingy( const char* stuff, Func helper)
    {
        int result = 0;
        //...call helper...
        return result;
    }
    

    If you want the function to be optional there is at least a couple options. One is to change the template to

    template <typename Func = int(*)(int),
              std::enable_if_t<std::is_invocable_r_v<int, Func, int>, bool> = true>
    int DoThingy2( const char*, Func helper = [](int i){ return internal_function(i); })
    {
        int result = 0;
        helper(result);
        return result;
    }
    

    which uses a default lambda to call your default internal function

    Another option is to have two overloads, one that takes the fucntion and one that does not but calls the one that does with your default function like

    template <typename Func, std::enable_if_t<is_invocable_r_v<int, Func, int>, bool> = true>
    int DoThingy( const char* stuff, Func helper)
    {
        int result = 0;
        //...call helper...
        return result;
    }
    
    int DoThingy( const char* stuff)
    {
        return DoThingy(stuff, internal_function);
    }