0

When used in a single scope, why does this compile:

float x = 3.14;
auto &&expr = 2*x + x*x; // expr binds to the result of an expression
expr = x;

Demo

First and foremost this looks like a reference re-binding, which is illegal for l-value references. Apart from that, expr is supposed to "be" the result of an expression. How can that "change"?

Lorah Attkins
  • 5,331
  • 3
  • 29
  • 63
  • 7
    _"First and foremost this looks like a reference re-binding"_ - Why do you feel it looks this way exactly? You can't rebind a reference, you are assigning to the underlying referenced variable. You could always do `auto& x = y; x = z;` -- it just assigns _the value of_ `z` to `y` (the referenced variable). The same is true here. The referenced value in question in this case is just a lifetime extended temporary value -- no different than `const`-reference extension. – Human-Compiler Jun 20 '22 at 19:25
  • 5
    You can't rebind a reference, full stop. What you are doing is assigning the value of `x` to the temporary object that `expr` refers to. – NathanOliver Jun 20 '22 at 19:29
  • @NathanOliver So I'm assigning `x` to `2*x + x*x` ? How/why does this compile? – Lorah Attkins Jun 20 '22 at 19:30
  • *"why does this compile"* Why not? Normally temporaries (of built-in types, at least) are protected from assignment by being rvalues, not by being const. `expr` is an lvalue, so this protection is stripped from it. – HolyBlackCat Jun 20 '22 at 19:32
  • @Human-Compiler Ok got you. So `expr` is not like a "numeral" or a "read-only" memory object. It's just an object that holds a result, and can be treated as such. – Lorah Attkins Jun 20 '22 at 19:32
  • 2
    @LorahAttkins `2*x + x*x` creates a temporary `int` object. `expr` then refers to that temporary object and because it does the compiler creates code to keep that object alive so long as `expr` is alive. – NathanOliver Jun 20 '22 at 19:34
  • @HolyBlackCat Ok, comments made that clearer. Somehow I memorized this happens for an "argument" (i.e. if I was to pass `expr` to a function) but detached that notion when written like in the same scope. I thought that the "function argument" is a "new object", so the rule "it's an l-value in that scope" applies. I see now this happens even when you don't pass the variable to other functions. – Lorah Attkins Jun 20 '22 at 19:35
  • UB galore: [reference rebinding](https://godbolt.org/z/8KcY1bKdx) – Ted Lyngmo Jun 20 '22 at 19:56
  • 1
    @TedLyngmo (Not UB) https://godbolt.org/z/hbdzGvfov – doug Jun 20 '22 at 22:32

1 Answers1

2

You can't rebind a top level reference just like you can't change a top level const. But when they exist in classes you can as of c++20. With c++20 you can rebind references but with significant gotchas. They, rvalue and lvalue refs, delete the defaulted assignment operator so you have to write custom member functions to make such a class generally usable.

Rebinding lvalue refs simply binds it to a different lvalue. rvalue refs bind to a tmp with no name and hence have special characteristics affecting the lifetime of the tmp objects they reference. Their lifetime ends when the scope of the original rvalue refs ends. While, if they are in a class, they can be rebound to a different rvalue the scope of that class object doesn't matter. Rather it's the scope of the initial class object that ends its lifetime.

You can not rebind a top level reference but, as of c++20 you can rebind one that exists in a class using std::construct_at.

Example lvalue ref:

#include <memory>
struct A { int& i; };
inline int f2(int i) {return 2 * i;}
int main() {
    float x = 3.14f;
    float&& expr = x * x + x * x; // expr binds to the result of an expression
    expr = x; // this isn't a rebind, just an assignment to an rvalue reference

    float& y = x;
    // no way to rebind y. It's an lvalue reference
    // and it cannot be changed to refer to another lvalue.

    // However, lvalue references that are inside a class can
    // be changed since an object of the class can be identified
    int i1{ 1 };
    int i2{ 2 };

    // lvalue reference in a class
    A a0{ i1 };
    A a1{ i2 };
    std::construct_at(&a0, a1); // a now contains a ref to i2;

Here's a program that demonstrates how rebinding an rvalue ref doesn't extend the ref's life even though the object it's rebound to has a longer life.

#include <memory>
#include <iostream>
struct B {
    inline static int seq{};
    int i;
    B() :i(seq++) {}
    ~B() {std::cout << "dtor " << i << "\n";}
};

B f(){return B();}

struct A {
    B&& rref;
};

int main() {
    {
        A a0{ f() };
        std::cout << "a0.rref.i=" << a0.rref.i << "\n";
        {
            A a1{ f() };
            std::construct_at(&a0, std::move(a1)); // a0.rref's tmp now disconnected replaced by a1.rref
            std::cout << "a0.rref.i=" << a0.rref.i << "\n";
            std::cout << "a1.rref.i=" << a1.rref.i << "\n";
        } // the tmp object referred to by both a0.rref and a1.rref, destroyed here when a1 goes out of scope
    } // the original tmp, which a0.rref n longer aliases, is destroyed here when a0 coes out of scope
}

/* Outputs *
a0.rref.i=0
a0.rref.i=1
a1.rref.i=1
dtor 1
dtor 0
***********/

Thanks to @HolyBlackCat for setting me straight that rvalue refs can, indeed be rebound. They can even refer to the same tmp object. But watch the lifetimes.

On another note, unlike, lvalue refs, I can't think of a use case for rebinding rvalue refs in a class. And only rarely for lvalue refs in a class.

doug
  • 3,840
  • 1
  • 14
  • 18
  • I don't see how lvalue vs rvalue reference is related to rebinding. Neither are rebindable. – HolyBlackCat Jun 21 '22 at 07:17
  • @HolyBlackCat Well, the posted code shows rebinding to an lvalue reference. There's good reasons to be able to do that since it allows class objects to be placed in containers and sorted, etc. As for rebinding rvalue references, I'm uncertain whether it can be done w/o UB nor do I see a use case for it. The concept of a container of objects where elements of each references an unnamed temporary boggles. – doug Jun 21 '22 at 15:00
  • It's not UB. It doesn't work in your code because rvalue references are move-constructible and not copy-constructible, so you'd need to move `b1`. – HolyBlackCat Jun 21 '22 at 17:08
  • @HolyBlackCat Thanks. You're right. And it makes sense. Two rvalue refs shouldn't and can't refer to the same tmp. This I can wrap my head around and can see how it would not be UB like a lvalue ref isn't UB. Of course multiple lvalue refs can refer to the same lvalue. I appreciate this moment of clarity. – doug Jun 21 '22 at 17:58
  • You're welcome. But they can refer to the same temporary. Moving a from a ref doesn't change it. – HolyBlackCat Jun 21 '22 at 18:00
  • @HolyBlackCat. No it doesn't change because it's a trivial class. However, even though it appears the same, it's technically no longer correct as would be clear were it not a trivial class. – doug Jun 21 '22 at 18:36
  • Nope. Moved-from objects don't become magically invalid. Non-class types remain unchanged when moved. The moved-from state of class types depends solely on their move constructor/assignment, and nothing else. Some standard library classes are said to be in a "valid but unspecified state" after moving, but this refers solely to how their move operations are written, and doesn't generalize to other types. – HolyBlackCat Jun 21 '22 at 18:42
  • @HolyBlackCat Sure moved from objects don't become automatically invalid. they are required to be valid but a move operation can alter them in any way it wishes so long as they remain valid. Nothing requires that a rvalue ref in such a class shall continue to refer to the same tmp. In the example case it does because it has a default move ctr which doesn't alter non-trivial sub-objects. – doug Jun 21 '22 at 19:24
  • Ok, so you *can* have two rvalue references point to the same object, and there's nothing illegal about it (such as in your example, if you add `move`). That's what I was arguing about. – HolyBlackCat Jun 21 '22 at 19:32
  • @HolyBlackCat Yep. In that example they refer to the same object. So we both agree now that both lvalue and rvalue refs in a class can be rebound w/o UB? – doug Jun 21 '22 at 19:40
  • Yep, I think we understand each other now. :) – HolyBlackCat Jun 21 '22 at 19:43
  • @HolyBlackCat. Thanks. Did a dive into lifetimes as well. Changed the answer to correct various issues. – doug Jun 22 '22 at 19:34