The following apparently results in a compilation error in C++23:
int& f(int& x) {
int y = ++x;
return y;
}
error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
But this one doesn't seem to even give us a warning:
int& f(int x) {
int &y = ++x;
return y;
}
Is this so much more difficult to catch during the compilation phase?
Thanks.
To understand the difference, one must understand why the code fails to compile in C++23.
Namely, in the first example, y is move-eligible because it names an implicitly movable entity and appears in a return statement. Therefore:
The expression is an xvalue if it is move-eligible (see below); an lvalue if the entity is a function, variable [...]
- [expr.prim.id.unqual] p1
An implicitly movable entity is a variable of automatic storage duration that is either a non-volatile object or an rvalue reference to a non-volatile object type.
- [expr.prim.id.unqual] p4
In the first example, the move-eligibility of y makes it an rvalue, and the returned reference cannot bind to it.
In the second example, y does not name an implicitly movable entity, so it's considered to be an lvalue, and the reference binding succeeds.
The fact that the first example doesn't compile is not because the C++ committee has made an effort to detect dangling references; instead, this is just a convenient side effect of some wording changes when it comes to implicit moves in return statements.
Detecting dangling references outside of this one, trivial case is very difficult. It generally requires lifetime annotations for functions, similar to Rust. Some proposals are focused on detecting simple cases at least.
For example, Herb Sutter has proposed standardizing analysis based on acyclic control flow graphs (ACFGs) in P1179: Lifetime safety: Preventing common dangling. This would at least make local analysis (within the same function) possible. The fact that this proposal is 47 pages long demonstrates that it's not a trivial issue though.
More trivial cases can be caught with @BrianBi's proposal P2748: Disallow Binding a Returned Glvalue to a Temporary
Is this so much more difficult to catch during the compilation phase?
The important thing to understand here is that C++ does not have a rule like "you shall not have a dangling reference" for the compiler to enforce. Instead, there are a bunch of more specific language rules that, when added up, catch some instances that would lead to dangling.
Specifically, in this example:
int& f(int& x) {
int y = ++x;
return y;
}
We now have a rule that says that return y; should treat y as an rvalue because it's a local variable (see Jan Schultke's answer for the specific rule and terminology), and you can't bind an rvalue to int&, so this doesn't compile. That's great, because had it compiled (like it used to), that would be a dangling reference.
However, in this example:
int& f(int x) {
int &y = ++x;
return y;
}
There just isn't a simple(~ish) rule to reject this function. y is already a reference, so returning it is fine. Binding the result of ++x to a reference is also fine. This is only wrong for a more complicated lifetime analysis - that ++x is an lvalue with the same lifetime of x, so returning y would exceed the lifetime of x. But there's no specific C++ rule here. You'd need to run a static analyzer to catch This.
Which, clang-analyzer does (but not clang or gcc with any warning that I can find):
<source>:3:5: warning: Address of stack memory associated with local variable 'x' returned to caller [clang-analyzer-core.StackAddressEscape]
1 | int& f(int x) {
| ~~~~~
2 | int &y = ++x;
3 | return y;
| ^ ~
<source>:3:5: note: Address of stack memory associated with local variable 'x' returned to caller
1 | int& f(int x) {
| ~~~~~
2 | int &y = ++x;
3 | return y;
| ^ ~
Similarly there was experimental work on more lifetime tracking (-Wlifetime) that caught this, although the warning you got was "returning a dangling pointer" which is maybe not the best text.
Otherwise - C++ isn't Rust in this sense. There just isn't such a "you shall not dangle" rule (which makes even more complicated examples involving containers and views and iterators much harder to properly diagnose). But given that we have an easy rule which rejects one dangling reference but no such easy rule for the other, I guess this is an existence proof that indeed one is much more difficult to catch than the other.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With