m.s. m.s. - 3 months ago 33
C++ Question

Passing lambda, which returns polymorphic unique_ptr, as function pointer

I want to pass a non-capturing lambda, which returns a

std::unique_ptr<Derived>
, as a function pointer of type
std::unique_ptr<Base>(*)()
.

However, this only works if I explicitly state the return type of the lambda as
std::unique_ptr<Base>
.


  • Why do I have explicitly state the return type?

  • Why does it work for a
    std::function
    without this extra return type?






#include <functional>
#include <memory>

struct Base{virtual ~Base()=default;};
struct Derived : Base{};

struct FailsForF2
{
using Function = std::add_pointer_t<std::unique_ptr<Base>()>;
FailsForF2(Function f) {}
};

struct Works
{
using Function = std::function<std::unique_ptr<Base>()>;
Works(Function f) {}
};


std::unique_ptr<Derived> fun() {return std::make_unique<Derived>();}

int main()
{

auto f1 = [](){return std::make_unique<Base>();};
auto f2 = [](){return std::make_unique<Derived>();};
auto f3 = []()->std::unique_ptr<Base>{return std::make_unique<Derived>();};

Works x1(f1);
Works x2(f2);
Works x3(f3);

FailsForF2 x4(f1);
FailsForF2 x5(f2);
FailsForF2 x6(f3);
}


gcc error:

main.cpp: In function 'int main()':

main.cpp:34:20: error: invalid user-defined conversion from 'main()::<lambda()>' to 'FailsForF2::Function {aka std::unique_ptr<Base> (*)()}' [-fpermissive]

FailsForF2 x5(f2);

^

main.cpp:26:17: note: candidate is: main()::<lambda()>::operator std::_MakeUniq<Derived>::__single_object (*)()() const <near match>

auto f2 = [](){return std::make_unique<Derived>();};

^

main.cpp:26:17: note: no known conversion from 'std::_MakeUniq<Derived>::__single_object (*)() {aka std::unique_ptr<Derived> (*)()}' to 'FailsForF2::Function {aka std::unique_ptr<Base> (*)()}'

main.cpp:10:4: note: initializing argument 1 of 'FailsForF2::FailsForF2(FailsForF2::Function)'

FailsForF2(Function f) {}


live example

Answer

TL;DR;

  • FailsForF2 fails because std::unique_ptr<Derived> (*) () is not implicitly convertible to std::unique_ptr<Base> (*) ();
  • Works works because std::unique_ptr<Derived> is implicitly convertible to std::unique_ptr<Base> (see the standard quotes at the end).

A lambda is implicitly convertible to a function pointer with the same return type and arguments1, so your three lambdas are respectively convertible to:

std::unique_ptr<Base> (*) ()
std::unique_ptr<Derived> (*) ()
std::unique_ptr<Base> (*) ()

Since std::unique_ptr<Derived> (*) () is different from (and not convertible to) std::unique_ptr<Base> (*) (), there is no viable overload for FailsForF2 constructor. See with the following piece of code:

std::unique_ptr<Derived> (*pfd) () = f2; // Compiles.
std::unique_ptr<Base> (*pfb) () = pfd;   // Does not compile (invalid conversion).

When you explicitly specify the return type of the lambda, you change the return type of the call operator of the lambda (the closure type associated to it actually, see the quote at the end), so the conversion is possible.


std::function on the other hand does not have such constraints - The constructor of std::function is templated so it can take any callable:

template <typename F>
std::function(F &&f);

...as long as the following is valid2:

INVOKE(f, std::forward<Args>(args)..., R)

1 Standard quote from N4594, §5.1.5/7 (emphasis is mine):

The closure type for a non-generic lambda-expression with no lambda-capture has a conversion function to pointer to function with C++ language linkage (7.5) having the same parameter and return types as the closure type’s function call operator. [...]

2 Standard quote from N4594, §20.12.12.2/2:

A callable object f of type F is Callable for argument types ArgTypes and return type R if the expression INVOKE (f, declval<ArgTypes>()..., R), considered as an unevaluated operand (Clause 5), is well formed (20.12.2).

...and §20.12.2 (emphasis is mine, 1.1 through 1.6 are about pointer (or alike) to member functions, so not relevant here):

1 Define INVOKE (f, t1, t2, ..., tN) as follows:

(1.x) - [...]

(1.7) - f(t1, t2, ..., tN) in all other cases.

2 Define INVOKE (f, t1, t2, ..., tN, R) as static_cast(INVOKE (f, t1, t2, ..., tN)) if R is cv void, otherwise INVOKE (f, t1, t2, ..., tN) implicitly converted to R.