Welcome to the first lesson of our second module, "Advanced C++ for Performance." In the previous module, we laid a solid foundation in modern C++, culminating in our exploration of templates and their power to enable static polymorphism for zero-cost abstractions. That was a key strategy for compile-time optimization.
Now, we shift our focus to a different but equally crucial area: runtime performance. Your goal of building a highly performative whiteboard application requires efficient management of dynamic resources—things like user-drawn strokes, rendered parts of a PDF, or cached images. Unnecessary copying of these heavy objects can cripple performance. This lesson introduces the concept that unlocks the solution: the distinction between lvalues and rvalues. Understanding these value categories is the first and most critical step toward mastering move semantics, a cornerstone C++ feature for eliminating expensive copies.
The Fundamental Distinction: Identity vs. Temporariness
At its core, every expression in C++ evaluates to a value that belongs to a specific category. For our purposes, the two most important categories are lvalues and rvalues.
- An lvalue (locator value) represents an object that has a persistent identity and a stable memory location. You can take its address.
- An rvalue (read-only value) represents a temporary value that is typically discarded at the end of the expression. It doesn't have a stable memory location that you can access.
The names originate from their positions in an assignment statement: an lvalue can appear on the left side of an assignment, while an rvalue can generally only appear on the right side.

Consider this simple code:
int a = 10; // 'a' is an lvalue. '10' is an rvalue.
int b = a; // 'b' is an lvalue. 'a' is also an lvalue.
int c = a + b; // 'c' is an lvalue. The result of 'a + b' is an rvalue.
a = 20; // OK: 'a' is an lvalue.
// 10 = a; // Error: Cannot assign to an rvalue (a literal).
// (a + b) = 42; // Error: Cannot assign to an rvalue (a temporary result).
This distinction isn't just academic; it allows the compiler to generate more efficient code. Since an rvalue is temporary, the compiler knows it doesn't need to be stored in memory in the same way an lvalue does. This insight is particularly relevant given your interest in low-level systems.
The following CppCon talk excerpt explains this low-level motivation beautifully. The speaker shows how rvalues can be encoded directly into machine instructions as "immediate operands," saving a memory access.
Back to Basics: Understanding Value Categories - Ben Saks - CppCon 2019
This segment from "Back to Basics: Understanding Value Categories" explains the 'why' behind the lvalue/rvalue distinction from a compiler's perspective.
Watch the portion from why this distinction exists. Pay attention to the assembly language examples. The idea that an rvalue like 1 can be part of the instruction itself, rather than requiring a memory lookup, is a key performance principle.
Interacting with Lvalues and Rvalues in Functions
This concept becomes truly powerful when we use it in function parameters. This is how we signal our intent to either read from a persistent object or take ownership of a temporary one.
Lvalue References (&)
You are already familiar with standard references, more formally called lvalue references. An lvalue reference acts as an alias for an existing object. As a rule, a non-const lvalue reference can only bind to an lvalue.
void printName(std::string& name) {
std::cout << name << std::endl;
}
std::string myName = "Alex";
printName(myName); // OK: myName is an lvalue.
// printName("Alex"); // Error: "Alex" is an rvalue literal.
The compiler prevents this because a function taking a non-const reference implies it might modify the object. Modifying a temporary literal that will be destroyed immediately is nonsensical and often a bug.
The const Reference Exception
There is a crucial exception: a const lvalue reference can bind to both lvalues and rvalues.
void printNameConst(const std::string& name) {
std::cout << name << std::endl;
}
std::string myName = "Alex";
printNameConst(myName); // OK: Binds to an lvalue.
printNameConst("Alex"); // OK: Binds to an rvalue.
This is why const references are so common for function parameters. They provide the efficiency of avoiding a copy while being flexible enough to accept any kind of input. The Cherno explains this rule and its historical context clearly.
This clip from "lvalues and rvalues in C++" explains the special rule allowing const lvalue references to bind to rvalues.
Watch from the special rule about const references. This is a fundamental C++ idiom you've likely used, and now you know precisely why it works.
Rvalue References (&&): Catching Temporaries
C++11 introduced rvalue references, denoted with a double ampersand (&&). Their purpose is to bind only to rvalues. This gives us a mechanism to specifically detect when a function is being called with a temporary object.
void processData(int&& data) {
// We know 'data' refers to a temporary.
}
int x = 10;
processData(10); // OK: 10 is an rvalue.
// processData(x); // Error: Cannot bind an rvalue reference to an lvalue 'x'.
This ability to distinguish between lvalues and rvalues allows us to create overloaded functions. One version can handle persistent objects (lvalues), and another can handle temporaries (rvalues).
These clips demonstrate how rvalue references enable function overloading and explain the optimization opportunity this creates.
First, watch how an rvalue reference (&&) allows you to write a function that only accepts temporary objects. Then, watch the explanation of how this is useful. The key idea is that if you know an object is temporary, you can "steal" its internal resources instead of copying them.
This "stealing" of resources is the essence of move semantics, which we will build in the next lesson. For your whiteboard app, imagine passing a large SkPath object representing a complex user stroke. If it's a temporary object, we can just take its internal point data. If it's a persistent object, we must make a copy to avoid corrupting it.
A Crucial Nuance: Named References are Lvalues
Here is one of the most important and initially confusing rules about value categories. Once an rvalue reference is bound to a name, that named variable is itself an lvalue.
Understanding lvalues, rvalues and their references
This article from Fluent C++ contains an exceptionally clear explanation of a tricky but essential concept.
Please read the section titled THE one thing that made it all click for me. The author explains why a parameter of type MyClass&& x, is treated as an lvalue inside the function f.
Why is this? Because inside the function, x has a name and a stable location on the stack for the duration of the function call. You can refer to it multiple times. It is no longer a "fleeting" temporary. It has an identity within that scope.
void process(std::string&& s) {
// 's' is of type "rvalue reference to string", but the expression 's' itself is an lvalue!
std::string another_string = s; // This performs a COPY, not a move.
// Because 's' is an lvalue here.
}
Forgetting this rule is a common source of bugs where developers expect a move to happen automatically but get a copy instead. We will see in a future lesson how std::move is used to solve this by casting an lvalue back into an rvalue reference.
The Complete Picture
The concepts of lvalue and rvalue have been refined in modern C++. The full picture includes three more categories: xvalue, prvalue, and glvalue.

You don't need to memorize all these terms right away, but it's good to see the complete map. The most important takeaway is that the fundamental divide is between lvalues (things with identity) and rvalues (things that are temporary or "expiring").
Conclusion
In this lesson, we have dissected the fundamental difference between lvalues and rvalues, a concept that underpins much of C++'s performance philosophy. This isn't just a theoretical exercise; it's the enabling mechanism for the powerful optimizations we'll explore next.
Key Takeaways:
- Lvalues have a persistent identity and memory location (e.g., variables). You can take their address.
- Rvalues are temporary values without a stable address (e.g., literals, results of expressions).
- This distinction allows the compiler to generate more efficient code, for instance by using immediate operands for rvalues.
- Lvalue references (
&) bind to lvalues. The specialconst T&form can bind to both lvalues and rvalues, making it a versatile tool for avoiding copies. - Rvalue references (
&&) bind only to rvalues. They allow you to write function overloads that specifically handle temporary objects. - The ability to detect temporaries is the key to optimization: you can safely "steal" resources from a temporary object instead of performing an expensive copy.
- A named variable, even one of type rvalue reference, is always an lvalue within its scope.
You now have the necessary vocabulary and conceptual model to understand move semantics. In our next lesson, we will put this theory into practice by implementing move constructors and move assignment operators, allowing our custom classes to participate in this powerful resource-management strategy.