cpp-rvalue-references

What Are R-value References in C++

  • 3 min

An R-value reference is a reference capable of binding to temporary values, i.e., objects that are usually about to disappear.

R-value references are written with && and are one of the foundations of modern C++. Thanks to them, the language can distinguish between objects we must preserve and objects whose resources we can move.

This article continues what we saw with L-values and R-values. If that distinction still feels a bit fuzzy, that’s fine—we’ll use it practically here.

Quick Reminder

In C++, an expression can behave as an L-value or an R-value.

A L-value is something with identity: a named variable, a recognizable memory location, something that still exists after the expression.

int x = 10; // x is an L-value
x = 20;     // we can assign another value to it
Copied!

An R-value is usually a temporary value: a literal, the result of an operation, or an object that will be destroyed soon.

int y = x + 5; // x + 5 is an R-value
Copied!

This difference allows us to tell the compiler: this is stable, copy it carefully or this is temporary, you can take advantage of its resources.

Declaring an R-value Reference

An R-value reference is declared with two ampersands (&&).

int&& ref = 42;

ref += 8;
std::cout << ref << "\n"; // 50
Copied!

Although it may seem odd, ref has a name. Therefore, inside the code, ref behaves as an L-value. This is important because it’s one of those details that initially seems like a bad joke.

void procesar(int&& valor) {
    // valor has a name, so here it behaves as an L-value
}
Copied!

Overloading Functions with & and &&

R-value references allow us to create different functions for persistent objects and temporary objects.

#include <iostream>

void procesar(int& valor) {
    std::cout << "L-value: " << valor << "\n";
}

void procesar(int&& valor) {
    std::cout << "R-value: " << valor << "\n";
}

int main() {
    int x = 10;

    procesar(x);      // L-value
    procesar(20);     // R-value
    procesar(x + 5);  // R-value
}
Copied!

This is the gateway to move semantics. If we receive an R-value, we know we can likely move its resources instead of copying them.

Relationship with std::move

std::move does not move anything by itself. What it does is convert an expression into an R-value, allowing move constructors or move operators to be called.

#include <string>
#include <utility>

std::string origen = "texto largo";
std::string destino = std::move(origen);
Copied!

After std::move, origen remains a valid object, but its content is left in a valid but unspecified state. You can destroy it or assign a new value to it, but you should not assume what it contains.

Do not use std::move out of habit. Use it when you want to transfer resources and you know you no longer need the current value of the original object.

An Example with a Vector

With large objects, moving can save a lot of work. Copying a std::vector involves duplicating all its elements; moving it usually involves transferring the internal buffer.

#include <iostream>
#include <utility>
#include <vector>

int main() {
    std::vector<int> datos = {1, 2, 3, 4, 5};

    std::vector<int> copia = datos;            // copies the elements
    std::vector<int> movido = std::move(datos); // transfers the resource

    std::cout << "movido: " << movido.size() << "\n";
}
Copied!

The difference is not noticeable with five integers, of course. But with a large buffer, images, huge strings, or system resources, avoiding unnecessary copies does matter.