Do Rvalue References Allow Dangling References

Do rvalue references allow dangling references?

Do rvalue references allow dangling references?

If you meant "Is it possible to create dangling rvalue references" then the answer is yes. Your example, however,

string middle_name () {
return "Jaan";
}

int main()
{
string&& nodanger = middle_name(); // OK.
// The life-time of the temporary is extended
// to the life-time of the reference.
return 0;
}

is perfectly fine. The same rule applies here that makes this example (article by Herb Sutter) safe as well. If you initialize a reference with a pure rvalue, the life-time of the tempoary object gets extended to the life-time of the reference. You can still produce dangling references, though. For example, this is not safe anymore:

int main()
{
string&& danger = std::move(middle_name()); // dangling reference !
return 0;
}

Because std::move returns a string&& (which is not a pure rvalue) the rule that extends the temporary's life-time doesn't apply. Here, std::move returns a so-called xvalue. An xvalue is just an unnamed rvalue reference. As such it could refer to anything and it is basically impossible to guess what a returned reference refers to without looking at the function's implementation.

Why is rvalue reference to an integer legal

A quote from cppreference

The lifetime of a temporary object may be extended by binding to a const lvalue reference or to an rvalue reference (since C++11), see reference initialization for details.

With a link to additional details here

Whenever a reference is bound to a temporary or to a subobject thereof, the lifetime of the temporary is extended to match the lifetime of the reference, with the following exceptions:

It then goes on to list a few exceptions like

a temporary bound to a return value of a function in a return statement is not extended: it is destroyed immediately at the end of the return expression. Such function always returns a dangling reference.

a temporary bound to a reference member in a constructor initializer list persists only until the constructor exits, not as long as the object exists. (note: such initialization is ill-formed as of DR 1696).
(until C++14)

a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call: if the function returns a reference, which outlives the full expression, it becomes a dangling reference.

a temporary bound to a reference in the initializer used in a new-expression exists until the end of the full expression containing that new-expression, not as long as the initialized object. If the initialized object outlives the full expression, its reference member becomes a dangling reference.
(since C++11)

a temporary bound to a reference in a reference element of an aggregate initialized using direct-initialization syntax (parentheses) as opposed to list-initialization syntax (braces) exists until the end of the full expression containing the initializer. (since C++20)

So the reasoning that

const int &ref = 1;

works since we use const is not true. The compiler is actually extending the lifetime of the temporary object to match the lifetime of the reference. So it's equally valid to do the same thing with an rvalue reference.

On the other hand

int &ref = 1;

would not make sense since 1 is not an lvalue. That's why we need const or an rvalue reference.

C++: Does the following code lead to dangling reference?

No it does not.

From my understanding, s should be a dangling reference because its assignment it binds to a return value of std::move

You have looked at the value categories, but not at the types.

The type of "test text" is char const[10]. This array reside in const global data.

You then pass it to move, which will return an rvalue reference to the array. The return type of this move cast expression is char const(&&)[10], an xvalue to a character array.

Then, this is assigned to a std::string&&. But this is not a string, but a character array. A prvalue of type std::string must then be constructed. It is done by calling the constructor std::string::string(char const*), since the reference to array will decay into a pointer when passing it around.

Since the reference is bound to a materialized temporary which is a prvalue, lifetime extension of the reference apply.

The std::move is completely irrelevant and does absolutely nothing in this case.

So in the end your code is functionally equivalent to this:

std::string&& s = std::string{"test text"};
std::cout << s << std::endl;

The answer would be difference if you would have used a std::string literal:

// `"test text"s` is a `std::string` prvalue
std::string&& s = std::move("test text"s);
std::cout << s << std::endl;

In this case, you have a prvalue that you send to std::move, which return an xvalue of type std::string, so std::string&&.

In this case, no temporary is materialized from the xvalue return by std::move, and the reference simply bind to the result of std::move. No extension is applied here.

This code would be UB, like the example you posted.

Dangling reference when returning reference to reference parameter bound to temporary

I like to keep an example class A around for situations like this. The full definition of A is a little too lengthy to list here, but it is included in its entirety at this link.

In a nutshell, A keeps a state and a status, and the status can be one of these enums:

    destructed             = -4,
self_move_assigned = -3,
move_assigned_from = -2,
move_constructed_from = -1,
constructed_specified = 0

That is, the special members set the status accordingly. For example ~A() looks like this:

~A()
{
assert(is_valid());
--count;
state_ = randomize();
status_ = destructed;
}

And there's a streaming operator that prints this class out.

Language lawyer disclaimer: Printing out a destructed A is undefined behavior, and anything could happen. That being said, when experiments are compiled with optimizations turned off, you typically get the expected result.

For me, using clang at -O0, this:

#include "A.h"
#include <iostream>

int
main()
{
A a{1};
A b{2};
A c{3};
A&& x = a + b + c;
std::cout << x << '\n';
}

Outputs:

destructed: -1002199219

Changing the line to:

    A x = a + b + c;

Results in:

6

Extension of the lifetime of a temporary with an rvalue reference

There is no need to extend any lifetime here: the object in question lasts until the end of main, which is after the end of foo.

C++ why int rvalue reference changed in this example?

Quoting Remy's answer from https://en.cppreference.com/w/cpp/language/reference_initialization#Lifetime_of_a_temporary.

"- a temporary bound to a reference parameter in a function call exists until the end of the full expression containing that function call: if the function returns a reference, which outlives the full expression, it becomes a dangling reference."

Both temporary objects are destroyed at the end of the full expression. The behavior is totally undefined here for both the int&& and string&&.

Why rvalue reference as return type can't be initialization of non-const reference?

I know that an rvalue reference is an lvalue.

You're talking about two different things: type and value category. e.g.

int&& ri = 0; // ri's type is rvalue reference (to int)
// ri's value category is lvalue; it's a named variable.

Given your 1st sample, what fun() returns is an xvalue, which belongs to rvalues.

The following expressions are xvalue expressions:

  • a function call or an overloaded operator expression, whose return type is rvalue reference to object, such as std::move(x);

then,

int &a = fun(); // fails; lvalue-reference can't bind to rvalue

In the 2nd sample, what fun() returns is an lvalue,

The following expressions are lvalue expressions:

  • a function call or an overloaded operator expression, whose return type is lvalue reference, such as std::getline(std::cin, str),
    std::cout << 1, str1 = str2, or ++it;

then

int & a=fun(); // fine; lvalue-reference could bind to lvalue

In the 3rd sample,

decltype(fun()) b = 1; // the return type of fun() is rvalue-reference;
// this has nothing to do with the value category of its return value
// b's type is rvalue-reference too, btw its value category is lvalue

In the 4th sample,

int &&a = 1; // fine; rvalue-reference could bind to rvalue
// a's type is rvalue-reference, its value category is lvalue
int &b = a; // fine; lvalue-reference could bind to lvalue
// b's type is lvalue-reference, its value category is lvalue

rvalue reference (expression) returned by function is xvalue - but no identity?

Does prvalue suddenly have "identity" if it's returned from a function as an rvalue reference?

Yes, actually. The standard pretty much says that outright:

[conv.rval]

A prvalue of type T can be converted to an xvalue of type T. This conversion initializes a temporary object ([class.temporary]) of type T from the prvalue by evaluating the prvalue with the temporary object as its result object, and produces an xvalue denoting the temporary object.

That temporary object, while it exists, most certainly has "identity". Of course, the result of such a conversion is no longer a prvalue, so perhaps we shouldn't say the prvalue "gets an identity." Note that this works, too, also because of temporary materialization:

(int&&)1; // This is different from f(), though, because that reference is dangling but I believe this one isn't (lifetime extension of a temporary by binding to a reference applies here but is suppressed for a return)

Note that the operand of a return statement and the thing that actually gets returned simply don't have to be the same thing. You give an int prvalue, you need an int xvalue, the return makes it work by materializing a temporary. It's not obliged to fail because of the mismatch. Unfortunately, that temporary immediately gets destroyed when the return statement ends, leaving the xvalue dangling, but, for that moment in between the returned reference being bound and the temporary being destroyed, yes, the rvalue reference indeed referred to an object with its own identity.

Other examples of prvalues being materialized so you can bind references to them:

int &&x = 1; // acts just like int x = 1 except for decltype and similar considerations
int const &y = 1; // ditto but const and also available in older C++ versions

// in some imaginary API
void install_controller(std::unique_ptr<controller> &&new_controller) {
if(can_replace_controller()) current_controller = std::move(new_controller);
}
install_controller(std::make_unique<controller>("My Controller"));
// prvalue returned by std::make_unique materializes a temporary, binds to new_controller
// might be moved from, might not; in latter case new pointer (and thus object)
// is destroyed at full-expression end (at the semicolon after the function call)
// useful to take by reference so you can do something like
auto p = std::make_unique<controller>("Perseverant Controller");
while(p) { wait_for_something(); install_controller(std::move(p)); }

Other examples of return not being trivial:

double d(int x) { return x; }
// int lvalue given to return but double prvalue actually returned! the horror!

struct dangerous {
dangerous(int x) { launch_missiles(); }
};
dangerous f() { return 1; }
// launch_missiles is called from within the return!

std::vector<std::string> init_data() {
return {5, "Hello!"};
}
// now the operand of return isn't even a value/expression!
// also, in terms of temporaries, "Hello!" (char const[7] lvalue) decays to a
// char const* prvalue, converts to a std::string prvalue (initializing the
// parameter of std::string's constructor), and then that prvalue materializes
// into a temporary so that it can bind to the std::string const& parameter of
// std::vector<std::string>'s constructor


Related Topics



Leave a reply



Submit