Odd return behavior with std::function created fro

2019-04-18 23:35发布

问题:

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!

回答1:

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.



回答2:

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.



回答3:

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>&.