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!
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.
I did a bit of my own searching regarding the std::function
constructor. It seems this part is an oversight in the interaction of std::function
and the standard's Callable
concept. std::function<R(Args...)>::function<F>(F)
requires F
to be Callable
as R(Args...)
, which in itself seems reasonable. Callable
for R(Args...)
requires F
's return type (when given arguments of types Args...
to be implicitly convertible to R
, which also in itself seems reasonable. Now when R
is const R_ &
, this will an allow implicit conversion of R_
to const R_ &
because const references are allowed to bind to rvalues. This is not necessarily unsafe. E.g. consider a function f()
that returns an int
, but is considered callable as const int &()
.
const int &result = f();
if ( f == 5 )
{
// ...
}
There is no issue here because of C++'s rules for extending the lifetime of a temporary. However, the following has undefined behavior:
std::function<const int &()> fWrapped{f};
if ( fWrapped() == 5 )
{
// ...
}
This is because lifetime extension does not apply here. The temporary is created inside of std::function
's operator()
and is destroyed before the comparison.
Therefore, std::function
's constructor probably should not rely on Callable
alone, but enforce the additional restriction that implicit conversion of an rvalue to a const
lvalue in order to bind to a reference is forbidden. Alternatively, Callable
could be changed to never allow this conversion, at the expense of disallowing some safe usage (if only because of lifetime extension).
To make things more complicated yet, fWrapped()
from the above example is perfectly safe to call, as long as you don't access the target of the dangling reference.
If you use:
return std::ref(in);
In your lambda it will work.
This will make your lambda's return type a std::reference_wrapper<std::vector<int>>
which is implicitly convertible to std::vector<int>&
.