Skip to content
VLSI Mentor

C++ Part I · Module 13 · Copy and Move Semantics

Value Categories: lvalues, rvalues and Why They Matter Now

Chapter 13.5 ended with a gap rather than a conclusion. The copy operations answer the question of what a duplicate of an object should own, and for a resource-owning class the answer is a second resource — correct, and sometimes pure waste, because the object being copied from is frequently one that nobody will ever look at again. A temporary returned from a function, an object being inserted into a container and then discarded, the result of concatenating two strings: in every one of those cases duplicating the resource and then destroying the original is work with no observable effect, and the resource could simply be handed over instead. Chapter 13.2 measured the price of not having that option as two copies per element minus one for a container filled without reserving. But a mechanism for handing over rather than duplicating needs something first: a way for the language to tell, at the point of a call, whether the thing being passed is an object somebody still owns or one that is about to disappear. That is not a property of the type — the same type appears in both situations — but of the expression, and this chapter is how C++ classifies expressions into those two groups and how you can observe the classification directly.

Advanced17 min readC++Part IFundamentalsValue Categories

Part I · Module 13 · Chapter 13.6 · Value Categories: lvalues, rvalues and Why They Matter Now

1. The Question

Does this expression name an object that will still exist after the expression finishes?

That question has two answers, and the two answers are the two value categories this chapter is about.

An lvalue names an object that persists. s, arr[3], obj.member, *p — each of these designates something with a name or a known location that outlives the expression.

An rvalue does not. It is a value in flight: the result of a function call returning by value, a temporary, a literal. After the expression that produced it, there is nothing left to refer to.

The names are historical — "left" and "right" of an assignment — and the historical reading is now actively misleading, so it is better to think of the question than the letters.

2. Why It Matters Now

The categories have existed since C, where they governed what you could assign to. What changed in C++11 is that they became the basis for an optimisation the language can apply safely.

If an argument is an lvalue, somebody still owns the object; taking its resource would leave them holding an empty shell they did not ask for. If it is an rvalue, nobody will look at it again; taking its resource is unobservable.

The value category is how the compiler knows whether stealing is safe.

Chapter 13.8 is the mechanism. This chapter is the classification it depends on.

3. The Operational Test

The most useful definition is not a list of cases but a question you can put to the compiler:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    std::string s;
    const std::string cs;
    std::string makeString();
Expression&expression
sOK
csOK
makeString()error: taking the address of a temporary object
std::string("tmp")error: taking the address of a temporary object

That error message is the definition. An rvalue has no address you may take because there is no persistent object to point at — which is precisely the frozen description of this chapter's "an object that will still exist afterwards", measured.

So the practical test is: if you can take its address, it is an lvalue.

(Chapter 8.2 established that & yields a pointer to an object; Chapter 10.2 §6 established that a pointer outliving its object is always wrong. A temporary's address would be that bug by construction, so the language refuses at the first step.)

4. The Classification, Measured

The second way to observe the category is to let overload resolution report it. Two overloads differing only in how they bind:

Azvya Education Pvt. Ltd.VLSI Mentor
valcat.cpp — the classifier
static void f(const std::string&) { std::printf("  lvalue  (const&)\n"); }
static void f(std::string&&)       { std::printf("  RVALUE  (&&)\n"); }

static std::string makeString() { return "made"; }

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
f(s)                  :   lvalue  (const&)
f(cs)                 :   lvalue  (const&)
f(makeString())       :   RVALUE  (&&)
f(std::string("tmp")) :   RVALUE  (&&)
f("literal")          :   RVALUE  (&&)
f(std::move(s))       :   RVALUE  (&&)
f(s + "x")            :   RVALUE  (&&)

Reading the rows:

s and cs are lvalues. Named variables, const or not. Constness and value category are independent — a const object is still an lvalue, which is worth stating because the two get conflated.

makeString() is an rvalue. A function returning by value produces a value, not a reference to anything that persists.

std::string("tmp") is an rvalue. An explicitly constructed temporary.

s + "x" is an rvalue. The result of an operator returning by value. This is the row that covers most real cases — every arithmetic or concatenation result is an rvalue.

std::move(s) is an rvalue, even though s is an lvalue. That is the entire purpose of std::move, and Chapter 13.9 is what it actually does — which is less than its name suggests.

And f("literal") needs care. A string literal is a const char[8], which is an lvalue of array type. It selected the && overload because the literal has to be converted to a std::string, and that conversion produces a temporary — and the temporary is the rvalue. So the row is about the conversion, not about literals being rvalues. Getting this wrong leads to the belief that string literals are rvalue std::strings, which is a different and incorrect idea.

5. The Pattern

Rather than memorising §4's rows:

Typically an lvalueTypically an rvalue
what it isa named object, or something you reached through a namea value produced and not stored
exampless, obj.m, arr[i], *p, csf(), T(...), a + b, std::move(x)
addressyou can take ityou cannot
after the expressionstill theregone

The one-line version: if it has a name, it is an lvalue. That is not exactly the rule but it is right often enough to be the first thing you reach for, and §6 is the important exception.

6. A Call Returning a Reference Is an lvalue

Section 5's rule — every by-value return is an rvalue — invites a wrong generalisation, which is that function calls produce rvalues. What a call produces depends on what the function returns, and the distinction is one you rely on constantly without noticing.

Measured, with the same two-overload probe:

ExpressionCategory
byValue() returning std::stringrvalue
byRef() returning std::string&lvalue
byCRef() returning const std::string&lvalue
v[0] on a std::vector<std::string>lvalue
v.at(0)lvalue

And &byRef() compiles, confirming it by §3's address test.

A function returning a reference produces an lvalue, because the reference names an object that already existed and will still exist afterwards — exactly §1's question, answered yes.

This is why the ordinary things you do with containers work:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    v[0] = "replaced";          // assigning to a call's result
    v.at(0).append("x");        // modifying it in place

Both require v[0] to be an lvalue, and it is, because operator[] returns std::string&. Chapter 11.7 §5 measured the two overloads of such an accessor — the non-const one returning int& and the const one returning const int& — and this is why that return type matters: it determines whether the caller gets something assignable.

So the refined rule is: a call is an rvalue if the function returns by value, and an lvalue if it returns a reference. The by-value case is the common one, which is why §5's shorthand is usually right, and the reference case is the one that makes containers usable.

7. The Exception That Causes Bugs

The rule in §5 has a consequence that surprises everyone once:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    void takesRvalueRef(std::string&& r)
    {
        f(r);                  // which overload?
        f(std::move(r));       // and this one?
    }

    takesRvalueRef(std::string("tmp"));

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  inside takesRvalueRef, passing r on:    lvalue
  ...and with std::move(r):                RVALUE

Inside the function, r is an lvalue. It is a named variable — it has a name, you can take its address, and it will still exist on the next line. The fact that its type is std::string&& does not make uses of it rvalues.

A named rvalue reference is an lvalue.

This matters because it is the commonest way a move is accidentally turned back into a copy: a function takes T&&, intending to move from it, and then passes the parameter onward by name — at which point the receiving function sees an lvalue and copies. The fix is std::move at the point of handing it on, and Chapter 13.7 is where the parameter form and this hazard are treated properly.

The underlying distinction is the one to keep: a type and a value category are different things. std::string&& is a type. lvalue and rvalue classify expressions, and an expression naming a variable of that type is an lvalue.

8. What This Buys

The classification exists so that the language can choose between duplicating and transferring without being told. Chapter 13.8 measures the result on the same container growth Chapter 13.2 measured:

copiesmovesallocations
copy-only type7011
type with a move constructor074

Four insertions of a sixteen-element buffer. The difference is entirely that push_back(Movable(16))'s argument is an rvalue, so the container is permitted to take its buffer instead of duplicating it — and the elements carried across each reallocation are also rvalues internally, for the same reason.

No call site changed between those two rows. The only difference is that the type offered a function for the rvalue case, and the classification routed the calls to it.

9. One Refinement, Named and Deferred

C++11 split these two categories into a finer scheme of five names, to handle cases this chapter has not needed — notably the difference between a temporary that has never been an object and an object being treated as expiring.

For everything in this module and everything in Part I, the two-way split is sufficient: either somebody still owns it, or nobody does. The finer scheme matters when writing generic code that must forward an argument while preserving its exact category, which is a template problem and belongs with Module 18's material.

It is named here so that the word prvalue in a compiler diagnostic does not look like a typo.

10. What to Actually Do

Ask whether anything still owns the object — that is the classification.

Use the address test when unsure: if &expr compiles, it is an lvalue.

Remember const has nothing to do with it — cs is a const lvalue.

Treat every by-value return and every operator result as an rvalue.

Remember a named T&& parameter is an lvalue — pass it on with std::move or it will copy.

Keep type and category separate — T&& is a type; lvalue and rvalue classify expressions.

Use the two-overload probe to find out what the compiler thinks of a specific expression.

And do not start sprinkling std::move — Chapters 13.9 and 13.12 measure where it helps and where it hurts.

11. Mistakes Worth Making Once

Reading the names literally. "Left and right of an assignment" is historical and now misleading.

Thinking a const object is an rvalue. Measured: f(cs) selected the lvalue overload.

Thinking a string literal is an rvalue std::string. Measured: it selects && because of the conversion, which produces the temporary.

Passing a named T&& parameter onward by name. Measured: it selects the lvalue overload and copies.

Treating the category as a property of the object. It is a property of the expression.

Treating T&& as meaning "this is an rvalue". It is a type; uses of the named variable are lvalues.

Trying to take the address of a temporary. error: taking the address of a temporary object — which is the definition rather than an obstacle.

Memorising the five-category taxonomy before needing it. The two-way split covers Part I.

12. Interview Relevance

"What is the difference between an lvalue and an rvalue?" An lvalue names an object that persists past the expression; an rvalue does not. The answer that lands adds the operational test — you can take an lvalue's address and not an rvalue's — rather than reciting "left and right of assignment".

"Is a const object an lvalue?" Yes. Constness and value category are independent, and this catches people.

"Inside void f(T&& x), is x an lvalue or an rvalue?" An lvalue — it has a name. This is the single best question in this area, because it separates understanding from memorisation, and the consequence is that passing x on without std::move copies.

"Why do value categories matter in modern C++?" Because they tell the compiler whether taking an object's resource is safe — nobody owns an rvalue, so stealing from it is unobservable. The measured 7-copies-to-0 container result is the concrete payoff.

"What is std::move(x)?" An expression of rvalue category referring to the same object — a cast, not a move. Chapter 13.9.

13. What You Should Be Able to Say Now

  1. The question: does this expression name an object that will still exist afterwards?
  2. An lvalue persists; an rvalue does not. The letters are historical and misleading.
  3. The categories are how the compiler knows whether taking a resource is safe.
  4. The operational test: &expr. Measured — &s and &cs compile; &makeString() gives error: taking the address of a temporary object.
  5. That error message is the definition.
  6. Measured by overload selection: s, cs → lvalue; makeString(), std::string("tmp"), s + "x", std::move(s), "literal" → rvalue.
  7. const is independent of category — cs is a const lvalue.
  8. "literal" selects && because of the conversion, not because literals are rvalues.
  9. Every by-value return and operator result is an rvalue.
  10. A named T&& parameter is an lvalue — measured: passing it on selected the lvalue overload.
  11. Type and category are different things — T&& is a type; the categories classify expressions.
  12. The payoff, measured: 7 copies and 11 allocations became 0 copies, 7 moves and 4 allocations, with no call site changed.
  13. A five-category refinement exists and is unnecessary for Part I.

14. Exercises

Exercise 1 — classify. lvalue or rvalue: x, x + 1, arr[2], f(), T{}, *p, p, std::move(x), obj.member, "text".

Exercise 2 — the address test. For each above, say whether &expr compiles, and give the error message for one that does not.

Exercise 3 — the named parameter. For void g(std::string&& s) { h(s); } say which overload of h is chosen and how to change it.

Exercise 4 — const. Explain why const std::string cs; makes cs an lvalue, and what would make an expression of that type an rvalue.

Exercise 5 — the literal. Explain why f("literal") selected the && overload, and why "string literals are rvalues" is the wrong lesson.

Exercise 6 — build the probe. Write the two-overload instrument from §4 and say what you would use it for.

Exercise 7 — the payoff. Explain why v.push_back(Movable(16)) can avoid a copy while v.push_back(existingObject) cannot.

Exercise 8 — ownership framing. Classify by asking "does something still own this": a returned std::string, a member of a local object, the result of a + b, a dereferenced pointer, a function parameter taken by value.

15. Summary

  • The question: will the object this expression names still exist afterwards? lvalue yes, rvalue no.
  • The categories tell the compiler whether taking a resource is safe.
  • Measured operational test: &makeString() gives error: taking the address of a temporary object — which is the definition.
  • Measured by overloads: named variables (const or not) are lvalues; by-value returns, explicit temporaries, operator results and std::move(x) are rvalues.
  • const is independent of category.
  • A string literal selects && via a conversion, not because literals are rvalues.
  • A named T&& parameter is an lvalue — the commonest way a move silently becomes a copy.
  • Type and category are different things.
  • Measured payoff: 7 copies / 11 allocations → 0 copies / 7 moves / 4 allocations.
  • A finer five-category scheme exists and is not needed here.

The classification is in place and nothing yet uses it. A function cannot currently say "I have a different and cheaper thing to do when my argument is an rvalue", because every parameter form available so far binds to both categories or to neither: a const T& parameter accepts an rvalue and then cannot modify it, and a plain T& parameter refuses rvalues outright. What is needed is a third form that binds only to the expressions nobody owns, so that a function overloaded on it is selected exactly when stealing is safe. The next chapter is that form, the table of what binds to what — which has one row that refuses something most readers expect it to accept — and the hazard that follows directly from the previous section's finding about named variables.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.