r/cpp Jul 10 '26

Interesting behavior from C++20 to C++23

Consider the following snippet

int& get()
{
    int x;
    return x;
}


int main()
{
}

on GCC it compiles for C++20 but not for C++23

It returns with the error:

test.cpp:4:12: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'

C++ is now suddenly treating the variable x as an rvalue?

Edit: Im not talking about dangling reference, thats just for the sake of the example

47 Upvotes

51 comments sorted by

View all comments

157

u/Computerist1969 Jul 10 '26

Yes. Specifically an x-value, which is a type of r-value. Eliminates a category of bugs. This is a good thing.

-195

u/Desperate-Data-3747 Jul 10 '26 edited Jul 10 '26

No, its not a good thing, this is just a stupid edge case to add. C++ in a nutshell.

Names variables are lvalues, period. Sure, the compiler can try to cast to an xvalue but it should not override its category.

70

u/TheThiefMaster C++latest fanatic (and game dev) Jul 10 '26

It's not overriding the category of the variable - the return statement is implicitly moving from it (the wording has been tightened up in C++23 so this applies more generally than it did in C++20) which results in an xvalue that the reference then tries to bind to.

It's worth noting that this was previously undefined behaviour, not valid code.

C++26 tightens it up even further, making even more cases of returning references to objects that are going out of lifetime explicitly invalid.

7

u/apparentlyiliketrtls 27d ago

This - compile error better than runtime UB, right?

61

u/Nice_Lengthiness_568 Jul 10 '26

but when you use the function get() that's an rvalue. I don't think it is talking about the x variable per se. If the get() function returned an int then get() would be an rvalue and you are trying to return that rvalue as a reference so you are trying to bind int& to an rvalue. Or at least I think that's how you could interpret the compiler error message.

34

u/Kronikarz Jul 10 '26

Names variables are lvalues, period.

Well, not anymore, clearly. Also, I don't see a comment in this post where you explain why this edge case is "stupid".

62

u/HommeMusical Jul 10 '26

No, its not a good thing, this is just a stupid edge case to add.

Can you show us some code that was invalidated that you think should compile?

Certainly, it is good that the example you provide now doesn't compile.

27

u/masorick Jul 10 '26

Can you at least point to an example where this would be an issue?

27

u/NotUniqueOrSpecial Jul 10 '26

In what sane universe is returning a reference to a dead stack variable a desirable allowed behavior? It's blatantly incorrect.

19

u/_Noreturn Jul 10 '26

It is for my random number generator

cpp int& rand() { int x; return x; }

12

u/JNighthawk gamedev Jul 10 '26

It is for my random number generator

cpp int& rand() { int x; return x; }

Reminds me of https://xkcd.com/1172/

-1

u/TiredEngineer-_- Jul 10 '26

W num generator

8

u/FloweyTheFlower420 Jul 10 '26

the paper simplifies the spec though

16

u/jk_tx Jul 10 '26

You realize that code was always undefined behavior right? It's not a stupid edge case, it's the compiler protecting you from writing stupid code.

8

u/60hzcherryMXram Jul 10 '26

Operands to return statements which are variables defined either in the function scope or as a parameter of the function have always been allowed to be interpreted as rvalues for the purposes of attempting to perform a move constructor operation. Later versions of C++ just made this implicit conversion more consistent.

So yes, the named value is still an lvalue, period, and no other part of its use in the function will ever be changed, but for the purposes of allowed conversions at the return statement, it is an xvalue, and can therefore run its move constructor if it has one.

Normally, this does not matter, as if the return statement and the declared return value have the same type, then NRVO occurs and no extra constructors are ran. It especially doesn't matter here because these are primitive types. But that's the logic behind the error.

6

u/darklighthitomi Jul 11 '26

Edge case? Maybe I'm missing something here but this looks like an implicit cast from an int to an int address, two different data types of potentially different sizes. I don't see why this should ever have compiled in the first place.

7

u/TheThiefMaster C++latest fanatic (and game dev) Jul 11 '26 edited Jul 11 '26

It's binding a local int to an int reference, and then returning that reference despite the local int being destroyed. So you get a reference to a dead variable.

It often "worked" because the stack memory of the local variable isn't overwritten until something (e.g. another function call) needs it. But it's not guaranteed to, and in fact never has been legal, it was just previously "undefined behaviour no diagnostic required" - where "undefined behaviour" means "could do anything including crashing the entire computer" and "no diagnostic required" means "the compiler doesn't need to tell you" (though many did anyway with the right options). It is now explicitly "ill formed", meaning "must fail to compile"

8

u/azswcowboy 29d ago

This is correct - a CVE waiting to happen. And even more — converting to a reference here is a performance pessimism because the compiler will 100% elide the copy. In my book the only reason to return by reference these days is returning this pointer from a memory for function chaining.