Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Odd return behavior with std::function created from lambda (C++)

I'm having trouble with std::functions created from lambdas if the function returns a reference but the return type isn't explicitly called out as a reference. It seems that the std::function is created fine with no warnings, but upon calling it, a value is returned when a reference is expected, causing things to blow up. Here's a very contrived example:

#include <iostream>
#include <vector>
#include <functional>

int main(){
   std::vector<int> v;
   v.push_back(123);
   std::function<const std::vector<int>&(const std::vector<int>&)> callback =
      [](const std::vector<int> &in){return in;};
   std::cout << callback(v).at(0) << std::endl;
   return 0;
}

This prints out garbage, however if the lambda is modified to explicitly return a const reference it works fine. I can understand the compiler thinking the lambda is return-by-value without the hint (when I originally ran into this problem, the lambda was directly returning the result from a function that returned a const reference, in which case I would think that the const reference return of the lambda would be deducible, but apparently not.) What I am surprised by is that the compiler lets the std::function be constructed from the lambda with mismatched return types. Is this behavior expected? Am I missing something in the standard that allows this mismatch to occur? I'm seeing this with g++ (GCC) 4.8.2, haven't tried it with anything else.

Thanks!

like image 333
Kevin Avatar asked Sep 30 '15 16:09

Kevin


1 Answers

Why is it broken?

When the return type of a lambda is deduced, reference and cv-qualifications are dropped. So the return type of

[](const std::vector<int> &in){return in;};

is just std::vector<int>, not std::vector<int> const&. As a result, if we strip out the lambda and std::function part of your code, we effectively have:

std::vector<int> lambda(std::vector<int> const& in)
{
    return in;
}

std::vector<int> const& callback(std::vector<int> const& in)
{
    return lambda(in);
}

lambda returns a temporary. It effectively is just copied its input. This temporary is bound the reference return in callback. But temporaries bound to a reference in a return statement do not have their lifetime extended, so the temporary is destroyed at the end of the return statement. Thus, at this point:

callback(v).at(0)
-----------^

we have a dangling reference to a destroyed copy of v.

The solution is to explicitly specify the return type of the lambda to be a reference:

 [](const std::vector<int> &in)-> const std::vector<int>& {return in;}
 [](const std::vector<int> &in)-> decltype(auto) {return in;} // C++14

Now there are no copies, no temporaries, no dangling references, and no undefined behavior.

Who's at fault?

As to whether this is expected behavior, the answer is actually yes. The conditions for constructibility of a std::function are [func.wrap.func.con]:

f is Callable (20.9.12.2) for argument types ArgTypes... and return type R.

where, [func.wrap.func]:

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.9.2).

where, [func.require], emphasis mine:

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

So, if we had:

T func();
std::function<T const&()> wrapped(func);

That actually meets all the standard requirements: INVOKE(func) is well-formed and while it returns T, T is implicitly convertible to T const&. So this isn't a gcc or clang bug. This is likely a standard defect, as I don't see why you would ever want to allow such a construction. This will never be valid, so the wording should likely require that if R is a reference type then F must return a reference type as well.

like image 188
Barry Avatar answered Dec 11 '22 16:12

Barry