Card 1 / 5
Every C++ expression is either an lvalue or an rvalue. An lvalue refers to an object that exists on its own and can be used again; an rvalue is a disposable result β a value with no life beyond the expression that produced it.
int x = 1;
int y = x + 1;
// `x` is an lvalue: it refers to an existing object β read it again, assign to it, take its address
// `x + 1` is an rvalue: a freshly computed value with no home of its own; used once, then gone
// `42` is an rvalue: a literal is a value, not an object you can refer toThe split matters because references pick sides: int& binds only to lvalues, int&& (C++11) only to rvalues β which is how a function can treat "a variable someone still needs" differently from "a temporary nobody will miss".
Rule of thumb: if the expression refers to something β a variable, arr[i], *p, a call returning T& β it is an lvalue. If it computes or constructs something fresh β arithmetic, a literal, std::string("a"), a call returning T by value β it is an rvalue.
int x = 1; int* p = &x; std::vector<int> v{1}; β which of these expressions is an rvalue?
x + 1Correct answerCorrect β an rvalue. The sum is a freshly computed value with no home of its own; it exists only inside this expression, and &(x + 1) does not even compile.
xAn lvalue: it refers to the variable x, an object that exists on its own and can be used again.
*pAn lvalue: dereferencing gives you the object p points to β here x itself β which lives on after the expression.
v[0]An lvalue: operator[] returns an int&, a reference to an element that keeps existing inside the vector; you can assign to it.
Card 2 / 5
An lvalue reference (int&) is another name for an existing object. Write larger, which returns a reference to whichever of its two arguments is bigger β so the caller can assign through the result and change the original variable.
a |
b |
returns a reference to | after larger(a, b) = 0 |
|---|---|---|---|
3 |
8 |
b |
a == 3, b == 0 |
13 |
0 |
a |
a == 0, b == 0 |
5 |
5 |
a β ties pick the first |
a == 0, b == 5 |
Return type int& plus return b; hands back the caller's own variable, not a copy. With a plain int return the caller would get a copy and larger(x, y) = 0 would not even compile β you cannot assign to a temporary. One rule to remember: never return a reference to a local variable, it dies when the function ends.
// Return an lvalue reference to whichever of `a` and `b` is larger
// (return `a` when they are equal). Because the result is a reference,
// the caller can assign THROUGH it and change the original variable.
int& larger(int& a, int& b) {
return (b > a) ? b : a;
}Card 3 / 5
Before C++11 an expression was either an lvalue (refers to an existing object) or an rvalue (a temporary). Move semantics needed one more distinction, because may it be moved from? and does it still refer to an existing object? are separate questions: std::move(x) may be moved from and still refers to x; x + 1 may be moved from and refers to nothing. So rvalues were split in two:
| expression | refers to an existing object? | may be moved from? | category |
|---|---|---|---|
x |
yes | no | lvalue |
std::move(x) |
yes | yes | xvalue ("expiring value") |
x + 1, 42, std::string("a") |
no | yes | prvalue ("pure rvalue") |
Two umbrella names group the rows:
How each arises: an lvalue is anything you can refer to (x, arr[i], *p); a prvalue is a literal or a fresh temporary; an xvalue is an lvalue that has been marked expiring β the result of std::move(x), or of any function returning T&&. std::move changes only the category; by itself it moves nothing.
std::string x = "hi"; β which statement about the expression std::move(x) is correct?
x.Correct answerCorrect. std::move(x) is a cast to std::string&&; the result still designates x (identity) but is marked as safe to move from β both halves of the xvalue definition.
x.No copy is made. The cast produces a reference to x itself, so the expression keeps x's identity β that is what separates an xvalue from a prvalue like x + "!".
x has a name.x alone is an lvalue; std::move(x) is not. The cast to std::string&& changes the expression's category to xvalue β which is precisely what lets a move constructor bind to it.
x empty.std::move moves nothing by itself. It only changes the category; a later construction or assignment that binds to the std::string&& is what actually takes x's buffer.
Card 4 / 5
An rvalue reference (std::string&&) binds only to rvalues β temporaries, and named variables wrapped in std::move. Write two overloads of classify so the compiler itself reports which kind of argument it received.
| argument expression | value category | result |
|---|---|---|
name (a named std::string) |
lvalue | "lvalue" |
std::string("tmp") |
rvalue β a temporary | "rvalue" |
std::move(name) |
rvalue β a cast | "rvalue" |
fixed (a const std::string) |
lvalue | "lvalue" |
Declare std::string classify(const std::string& s) and std::string classify(std::string&& s). Overload resolution prefers the && overload whenever the argument is an rvalue; const& accepts everything else. std::move(name) moves nothing by itself β it is a cast that makes a named variable look like an rvalue so the && overload gets picked.
// Two overloads of the same function. The compiler picks one by the
// VALUE CATEGORY of the argument:
// - `const std::string&` binds to lvalues (named objects) -> return "lvalue"
// - `std::string&&` binds to rvalues (temporaries, moves) -> return "rvalue"
// Write both overloads of `std::string classify(...)`.
std::string classify(const std::string& s) {
return "lvalue";
}
std::string classify(std::string&& s) {
return "rvalue";
}Card 5 / 5
Move semantics let an object steal another object's resources β a heap buffer, a file handle β instead of copying them. The stealing happens inside a move constructor or move assignment operator, and those are only chosen when the source expression is an rvalue.
std::string a = "a long string that lives on the heap";
std::string b = std::move(a); // b takes over a's buffer; a is left emptystd::move contains no loop, no memcpy, nothing that touches the object. It is static_cast<T&&>(x) β a cast to rvalue reference. That cast is what lets overload resolution pick the move constructor instead of the copy constructor. Whether anything is actually moved depends entirely on the destination type having a move constructor that does something.
What does the expression std::move(x) do, by itself?
x to an rvalue reference so a move constructor or move assignment can be selected; it moves nothing itself.Correct answerCorrect. std::move is essentially static_cast<T&&>(x). The actual transfer of resources happens later β in whatever move constructor or move assignment the cast allows the compiler to pick. If nothing consumes the result, nothing moves.
x's contents into a temporary and then clears x.Incorrect. No copy and no clearing happens in std::move. Emptying the source (if it happens at all) is the job of the destination's move constructor, and it steals rather than copies.
x's move constructor, transferring its resources.Incorrect. A move constructor runs when a new object is constructed from an rvalue. std::move(x) only produces that rvalue; T y = std::move(x); is where the move constructor runs.
x's resources so the object can be reused.Incorrect. Nothing is released. x is untouched until some move operation actually consumes it β and even then it is left in a valid state, not destroyed.