Why does C++ prohibit binding T1 && to T2 lvalue when T1 is reference-related to T2 (and T1 and T2 are not class types)?

177 Views Asked by At

I'm exploring this question: Given char * x, why doesn't const char * && rc2 = x compile?

And I found dcl.init.ref#5.4.4 which says (when we are binding T1 && to T2 lvalue where T2 is not class type, ) and if T1 is reference-related to T2, then if the reference is an rvalue reference, the initializer expression shall not be an lvalue.

Why is there a rule like this? What does this rule want to avoid?


I clearly know that bindings like int i; int && ri = i; is illegal, but my question here is, I expect const char * && rc2 = static_cast<const char *>(x); where static_cast<const char *>(x) should be an prvalue which can bind to rvalue references.

As int i; double && rd = i; is valid, why is char * x; const char * && rc2 = x; invalid?

2

There are 2 best solutions below

0
Brian Bi On BEST ANSWER

This rule covers not only cases such as

int x;
int&& r = x;

but also

const int x = 0;
int&& r = x;

Here, we don't have an exact match in types: the reference wants to bind to an int, but the initializer expression has type const int. Just like how we don't want the first example to create a temporary int object (a copy of x) and then bind r to that, in the second example we also don't want a temporary int object to be created by copying x.

Or, similarly

struct Base { /* ... */ };
struct Derived : Base { /* ... */ };
Derived d;
Base&& b = d;

The idea is that if you have a reference binding that could have been a direct binding if only the reference were of the appropriate kind (i.e. an lvalue reference instead of an rvalue reference) and had the appropriate cv-qualification, then it's probably the programmer's mistake. Creating a temporary object is usually not the desired behavior.

Reference-relatedness captures this notion of "could have been a direct binding".

As for this case that you mentioned:

char * x; const char * && rc2 = x;

the same logic applies: the reference-relatedness tells you that this is "too close" to a valid reference binding. If you changed the type of the reference to const char* const&, then the binding would be valid.

8
Enlico On
The question seemed to boil down to

why reference-related types are specially treated? :)

I don't know the answer to this, and I suspect that it's not explicitly written in the standard.

I can make the wild hypothesis:

  • If x and y have reference-related types, that means you can lay them out in memory in the same way, so having x be a reference to y is possible, hence a thing like X && x = y; would surprise you, as editing x would change y, which you would not expect from an rvalue reference (unless you had consciously std::moved y, clearly).
  • On the other hand, if x and y have types which are not reference-related, then a thing like X && x = y; requires a conversion of y from Y to X so you truly get a temporary, layed out somewhere else in memory, to which X&& can bind.
Below is the answer to the original question plus some of its edits

Given char * x, why const char * && rc2 = x doesn't compile?

Because that's like any other SomeType x; const SomeType && r = x;, i.e. it's trying to bind an lvalue to an rvlue reference, and that's not allowed for the piece of standard you quoted.

My question is, why there is a rule like this? What does this rule want to avoid?

It's not that it wants to avoid something. It's just the way of making the rvalue references actually useful.

After all,

  • If an rvalue reference could bind to both rvalues and lvalues, it would be no different from const&, so why introducing it?
  • If an rvalue reference could bind only to lvalues, it would be no different from (non-const)&, so why introducing it?

int i;
double && rd = i;

works because rd is a reference to double, but the initializer is an int, so a temporary double is materialized by converting i to double, and that is a temporary.

Notice that after those two statements, you shouldn't expect, and indeed it doesn't happen, that modifying rd results in modifying i.

On the other hand, the case you mention originally, is more akin to (the reason being that even though the types are different in your case, char* vs const char*, they are still reference-related, because of conv.qual)

int i;
int && rd = i; // illegal

If this was to work, then the two bullet points above would apply, and you'd be seeing the edits to rd be reflected in i.

However, if you were to do this

int i;
int && rd1 = static_cast<int>(x);
int && rd2 = static_cast<int&&>(x);

rd1 is bound to a temporary (because of expr.static.cast#1), not i, whereas rd2 has write access to i.