6

Is below code standard-correct? (godbolt)

I.e. by-ref capturing a forwarding reference that represents a temporary, and returning the resulting lambda by-value from the function, within the same expression.

Of course storing the lambda for later use would make it contain a dangling reference, but I'm referring to the exact usage inside main.

The doubts I'm having relate to this SO answer and potentially this language defect. Specifically there is one daunting comment that says "the reference capture lifetime rule in the standard references captured variables, not data, and their scope" - this appears to say a captured reference to a temporary might be invalid in my code.

#include <stdlib.h>
#include <string.h>
#include <cassert>

template<typename F>
auto invoke(F&& f)
{
    return f();
}

template<typename F>
auto wrap(F&& f)
{
    return [&f]() {return f();}; // <- this by-ref capture here
}

int main()
{
    int t = invoke(wrap(
        []() {return 17;}
    ));

    assert(t == 17);
    return t;
}
HolyBlackCat
  • 78,603
  • 9
  • 131
  • 207
haelix
  • 4,245
  • 4
  • 34
  • 56
  • 2
    Is this your actual use case? Cause you could just do `return f();` of course. – Hatted Rooster Apr 07 '19 at 13:06
  • just checking. You realise that those universal references in `invoke` and `wrap` are not being propagated, right? You may as well pass lvalue references. – Richard Hodges Apr 07 '19 at 13:28
  • @SombreroChicken in the actual use case `wrap` does something, of course – haelix Apr 07 '19 at 13:37
  • @RichardHodges yes, I could declare `invoke` to take l-value ref but it won't change the question - the `invoke` function simply serves as a way to use the lambda returned from `wrap` inside the same expression – haelix Apr 07 '19 at 13:39

2 Answers2

9

There was UB in your code for a relatively short historical time window. (Note: this is a very strange thing to say). The original lambda capture-by-reference rules stated the reference was only valid until the variable captured went out of scope.

There is no UB in your code under both the current standard, and under every retroactively revised past standards.

This could lead to a sort of capture reference-by-reference, otherwise impossible in the C++ standard. (The closest you could get would be a reference to a one-member struct containing a reference)

In theory, you could use that fact to make lambda reference capture be stack-frame based; capture the current stack frame, and all (almost?) by-reference arguments would be at fixed offsets to that stack frame.

As most (all?) ABIs implement reference arguments as pointers under the hood, this would lead to reference arguments to functions arguments which are references dangling after the lambda returned.

No compiler exploited this fact. That optimization was never used, it was just observed as possible. The "reference capture of a lambda has a lifetime of the variable reference" rule was never exploited by any compiler (or at least any I heard of).

When it was spotted, it was resolved as a defect resolution in the standard, which means it retroactively redefined what meant.

So while under historical compiler this was technically UB, no current compliant compiler can treat it as UB, and all historical C++11 compilers treat it the same way current compilers do. So you are safe.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • 1
    Brilliant answer with inclusive history lesson. Legendary. – Richard Hodges Apr 07 '19 at 15:15
  • I am quite humbled by this answer, I have been programming almost exclusively C++ for the last 14 years in professional environments, the large majority of it in finance/low latency etc. I believe I have enough information to mark this as the answer but I'll try to ramp up on the subject. – haelix Apr 08 '19 at 01:08
  • I think you mean "There was UB in your code for a relatively short window." by history, not program execution time window. right? the wording is a little confusing to me (non-native english speaker, fwiw). maybe assert that there is no UB in current standard at beginning would help. – apple apple Nov 28 '22 at 15:07
  • 1
    @appleapple Yes, the UB was in a historical time window. And in that historical time window, as far as I know, no compiler *noticed* the UB, and when the standard was retroactively fixed to make it defined behavior, no compiler *changed* how it handled it. – Yakk - Adam Nevraumont Nov 28 '22 at 15:11
  • @Yakk-AdamNevraumont thanks :) it's more clear now. – apple apple Nov 28 '22 at 15:27
2

Yes there is no UB in your code. f is bound to the lambda, but you invoke the lambda which captures f in the same expression, so its lifetime has not ended. The defect report you linked clarifies what capturing a reference by reference means. It was resolved by clarifying that the reference capture is actually a reference to the object the captured reference is bound to.

In your case, the captured f is a reference to the lambda (instead of to the parameter f).

Rakete1111
  • 47,013
  • 16
  • 123
  • 162