lvalue and rvalue references

65 Views Asked by At

I am having a hard time understanding lvalue and rvalue references. Look at this code example

#include <iostream>

//Returns r-value
int Add(int x, int y) {
    return x + y;
}
//Return l-value
int & Transform(int &x) {
    x *= x;
    return x;
}

void Print(int &x) {
    std::cout << "Print(int&) L value ref" << std::endl; 
}
void Print(const int &x) {
    std::cout << "Print(const int&) const l value ref" << std::endl;

}
void Print(int &&x) {
    std::cout << "Print(int &&) r value ref" << std::endl;
}
int main() {
    //x is lvalue
    int x = 10;
    
    //ref is l-value reference
    int &ref = x ;
    //Transform returns an l-value
    int &ref2 = Transform(x) ;
    //Binds to function that accepts l-value reference
    Print(x);

    Print(ref);
    Print(ref2);

    //rv is r-value reference
    int &&rv = 8 ;
    
    //Add returns a temporary (r-value)
    int &&rv2 = Add(3,5) ;
    //Binds to function that accepts a temporary, i.e. r-value reference
    Print(3);
    Print(rv);
    Print(rv2);
    return 0;
}

When I run this I got

Print(int&) L value ref
Print(int&) L value ref
Print(int&) L value ref
Print(int &&) r value ref
Print(int&) L value ref
Print(int&) L value ref

I can (I guess) understand the first 4. However for the last two I am printing rv and rv2 which I thought were r-values so why are they being passed to the Lvalue ref function?

2

There are 2 best solutions below

0
Remy Lebeau On

rv and rv2 are actually l-values, because they have names assigned to them. They both have r-value reference as their type.

The 1st Print() takes a const-reference, which can bind to both lvalues and rvalues. To call the 2nd Print() instead, you will have to use std::move() to convert rv and rv2 into r-values, eg:

Print(std::move(rv));
Print(std::move(rv2));

Online Demo

3
goober On

From cppreference.com:

The functions that accept rvalue reference parameters [...] are selected, by overload resolution, when called with rvalue arguments. If the argument identifies a resource-owning object, these overloads have the option, but aren't required, to move any resources held by the argument.

In your code example, namely

//rv is r-value reference
int &&rv = 8 ;

//Add returns a temporary (r-value)
int &&rv2 = Add(3,5) ;

the && aren't r-value references but universal references. That means the type could be either an rvalue or an lvalue, whenever is appropriate. In your case, the compiler selected an lvalue.

You need to force the category to be an rvalue by performing std::move:

Print(std::move(rv));
Print(std::move(rv2));

I've linked the cppreference page to std::move previously but I've remembered it by the phrase "convert an lvalue to an rvalue". Here's another example:

#include <iomanip>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
 
int main()
{
    std::string str = "Salut";
    std::vector<std::string> v;
 
    // uses the push_back(const T&) overload, which means
    // we'll incur the cost of copying str
    v.push_back(str);
    std::cout << "After copy, str is " << std::quoted(str) << '\n';
 
    // uses the rvalue reference push_back(T&&) overload,
    // which means no strings will be copied; instead, the contents
    // of str will be moved into the vector. This is less
    // expensive, but also means str might now be empty.
    v.push_back(std::move(str));
    std::cout << "After move, str is " << std::quoted(str) << '\n';
 
    std::cout << "The contents of the vector are {" << std::quoted(v[0])
              << ", " << std::quoted(v[1]) << "}\n";
}
After copy, str is "Salut"
After move, str is ""
The contents of the vector are {"Salut", "Salut"}

Correction: int&& is indeed an rvalue reference. auto&& is a universal reference, which in your question, is irrelevant. However, it's useful to know they exist.