Sandrino's WEBSITE

   ABOUT  BLOG  


C++ Rvalue and Lvalue

This blog post should be a relative simple explanation of what rvalue and lvalues are. It will not go in depth but it should be enough for a easy understanding, and enough if it every comes up.

Definition

First and easiest explanation is, that a Lvalue is always on the left side of an assignment and and Rvalue always on the right side of an assignment. In the example bellow we see the most simple explanation of Lvalue and Rvalue. I created a define statement for the rvalue.

#define rvalue 10
int lvalue = rvalue
int otherLvalue = 42 // 42 is the rvalue
int thirdLvalue = lvalue + otherLvalue // The addition is also an rvalue

So now we know where an lvalue and rvalue can be, but what other differences to they have.

So with this new information we can also show some different examples.

int lvalue = 10;
int* pLvalue = &lvalue; 

lvalue is in our example a variable which gets a specific address in memory. The value 10(literal constant) is our rvalue it has no specific address in memory. We can get the address of lvalue with the & operator which takes an lvalue as argument and returns a rvalue. So in this case the expression '&lvalue' is a rvalue.

We can also try the other way as shown in the example bellow

int x;
123 = x;

The example obviously does not work. Because 123 is a constant literal or rvalue and has no specific address so we can not assign a value to something which has no place to live. With clang++ we would get such an error error: expression is not assignable

We can also try to grab the address of an rvalue

int* x = &123;

Clang++ would tell us: error: cannot take the address of an rvalue of type 'int'. Which was to be expected because I mentioned earlier, that rvalues have no specific address and the & operator expects an lvalue.

Functions

So far I only talked about simple examples with variables, but what happens in the context of functions?

int val()
{
    return 123;
}

val() = 1;  // Error

As mentioned already on the left side of an assignment we have always an lvalue. clang++ would say the following: error: expression is not assignable as we can see we get the same error as above. Because the the value returned by the function has no address and is itself only a rvalue.

Is it even possible to make the example above work? The answer is yes, we could use the help of references to be able to not return a rvalue from the function but to reference an actual variable which has an address in memory.

int global = 123;

int& val()  // Changed signature to return a reference
{
    return global;
}

val() = 1;

The example above works because we return a reference which points to an address in memory in our case the variable global.

Lvalue to Rvalue conversion

In C++ there are often situations where lvalue to rvalue conversions happen under the hood.

int z = x + y;

In this addition we have an implicite conversion from the lvalues x,y to rvalues. Because the + operator expects two rvalues as we can see in the specification: T operator+(const T &a, const T2 &b);. From what I understand the compiler would fetch the values of the address of x and y and then pass it to the + operator which in turn also returns an rvalue.

For fundamental types such as int's the conversion is done automagical, but for custom types we would have to implement such a conversion ourselfs. One example would be:

class MyType {
public:
    MyType(int value) : data(value) {}

    operator int() const {
        return data;
    }

private:
    int data;
};

int main() {
    MyType obj(10);  // Create an object of MyType.
    int value = obj; // 'obj' is implicitly converted to an 'int' using the conversion operator.
    // The value of 'obj' (10) is extracted and assigned to 'value'.
    
    return 0;
}

As we can see we implemented the conversion to int explicitly with int() which returns an rvalue. But in general those conversions are done implicitly and we do not have to care to much.

Another implicite conversion is:

int arr[5] = {1, 2, 3, 4, 5};
int element = arr[2]; // 'arr[2]' is an lvalue, but it's converted to an rvalue for assignment to 'element'.

Lvalue References

We are able to reference lvalues with:

int x = 10;
int& y = x;
++y;

In this case we reference the variable x via y (With the operator int& a reference to x). We ensure that y is referencing a. A reference has always to reference an existing object on a specific address. We would not be able to do something like this:

int& y = 123;

clang++ would greet us with: error: non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int' (When we read the error message we should see that clang++ is mentioning something with 'const' this will be important later)

The following example will also not work we can not pass const literals to functions which expect references:

int refPar(int& x)
{

}

intPar(123);

In this case we would get following compiler message: note: candidate function not viable: expects an lvalue for 1st argument with an additional error message mentioning that we have no matching function call. As we can see it is not possible to pass a const literal to a function which expects a reference.

Const value reference

As I mentioned before, clang was telling us something about const. So how can we use that to make the example from above work? Is is allowed to bind const lvalue to rvalues. Fixed example from above:

int refPar(const int& x)
{

}

intPar(123);

Now the example works as we hoped. But how? The answer to that is that if we use the example without the const then we reference something that only exists for a short moment of time and it makes no sense to reference that. But if we use a const then we tell the compiler, that we will not modify that value. Based on that the compiler will create a hidden temporary variable for us:

To be able to see what happens under the hood we can try following example:

int main()
{
    const int& ref = 420;
    return 0;
}

This would give us following assembly output:

_main:                                  ; @main
        .cfi_startproc
; %bb.0:
        sub     sp, sp, #32
        .cfi_def_cfa_offset 32
        mov     w0, #0
        str     wzr, [sp, #28]
        add     x8, sp, #12
        mov     w9, #420        <----- Loading the value 420 into register w9
        str     w9, [sp, #12]   <----- Storing value of w9 on the stack, this is used later for the reference
        str     x8, [sp, #16]
        ldr     x8, [sp, #16]
        ldr     w8, [x8]
        str     w8, [sp, #8]
        add     sp, sp, #32
        ret
        .cfi_endproc
                                        ; -- End function

In other words the compiler would generate something like his:

int __internal_unique_name = 10;
const int& ref = __internal_unique_name;

Sources