Skip to content
VLSI Mentor

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

Copy Elision and Returning Objects by Value

The previous chapter's conclusion rests on something it did not justify. The Rule of Zero works because composed objects can be handed around by value — returned from factories, produced by builders, passed out of functions that assemble them — and on the face of it every one of those returns is a copy of something that may be large. Chapter 6.3 recommended returning by value, Chapter 10.2 insisted on it over every alternative including allocation, and then this module spent eleven chapters establishing that copies are exactly the thing to worry about. Those positions need reconciling, and the reconciliation is usually offered as folklore: the compiler optimises it away, do not worry. That is true of some cases, false of others, and the difference matters because one of the five ordinary ways of writing a return statement produces a real copy at every optimisation level, and another produces an unnecessary move that the compiler will warn you about if you let it. This chapter measures all five at three build settings, including the one compiler flag whose entire purpose is to switch such optimisations off — which turns out to be the only way to tell empirically which eliminations the standard requires and which it merely permits.

Advanced17 min readC++Part IFundamentalsCopy Semantics

Part I · Module 13 · Chapter 13.12 · Copy Elision and Returning Objects by Value

1. The Question

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    std::vector<Sample> load();          // returns by value
    auto samples = load();               // a copy of the whole vector?

Chapter 6.3 said return by value. Chapter 10.2's callout said it over every alternative. Chapter 13.11's Rule of Zero depends on it. And Chapter 13.2 measured that copies cost allocations.

The answer is that the copy usually does not happen — and "usually" is not good enough, so this chapter establishes which cases are guaranteed.

2. What Elision Is

Copy elision is the compiler constructing an object directly in its final location instead of building it somewhere else and then copying or moving it there.

Not an optimisation of the copy. The removal of the copy, by never having two objects in the first place.

This is unusual among optimisations in two ways:

It is observable. Eliding a copy removes a call to a constructor you wrote, which may print or count. Chapter 13.2's counting instrument sees the difference, and that is what makes it measurable at all.

And in some cases it is required, not permitted. That is the distinction §3 and §4 separate.

3. The Guaranteed Case

Since C++17, when a function returns a temporary and the caller initializes an object from the result, there is no copy to elide — the object is required to be constructed directly in the destination:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Obj returnTemporary() { return Obj(1); }
    Obj r = returnTemporary();

Measured at three settings:

Buildcopiesmoves
-O000
-O200
-fno-elide-constructors00

The third row is the proof. That flag exists to disable this class of optimisation, and it cannot disable this one — because in C++17 this is not an optimisation. The standard describes the returned expression as initializing the caller's object directly; there is no intermediate object, so there is nothing for the flag to restore.

Chapter 12.10 §7 measured the same thing from a different angle: seven objects in a traced function, 0 copy constructor calls, unchanged by that flag.

So this shape is free, guaranteed, and not a matter of compiler quality.

4. The Permitted Case

Returning a named local is different:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Obj returnNamedLocal() { Obj x(2); return x; }
Buildcopiesmoves
-O000
-O200
-fno-elide-constructors01 move

The flag changed this row. At ordinary settings the local is constructed directly in the caller's storage and nothing is copied or moved; with elision disabled, a move appears.

That tells you exactly what the status is: the elimination here is permitted, not required. This case has a name — the named return value optimisation — and every mainstream compiler performs it at every optimisation level including -O0, as measured. But the standard allows a compiler not to, and if it does not, the fallback is a move rather than a copy (because the returned local is treated as expiring).

So this shape is free in practice and a move in the worst case — which for a composed type is two assignments, not an allocation.

5. The Shape That Copies

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Obj returnTwoPaths(bool b) { Obj p(3); Obj q(4); return b ? p : q; }
Buildcopiesmoves
-O01 copy0
-O21 copy0
-fno-elide-constructors1 copy0

A genuine copy, at every setting, and it is a copy rather than a move.

Two reasons, and both come from earlier chapters:

Two candidate objects. The compiler would have to construct one object in the caller's storage, and it cannot know which until the condition is evaluated — by which time both already exist as locals. So there is nothing to elide.

And the returned expression is an lvalue. b ? p : q names one of two existing objects; Chapter 13.6 §5's rule applies — it has a name, so it is an lvalue, so the copy constructor is selected rather than the move constructor.

The fix, if this matters, is to make the returned expression a single object:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Obj returnOnePath(bool b) { if (b) return Obj(3); return Obj(4); }   // two temporaries, both elided

Each return now returns a temporary, which is §3's guaranteed case.

6. The Shape That Makes Things Worse

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Obj returnMoved() { Obj x(5); return std::move(x); }
Buildcopiesmoves
-O001 move
-O201 move
-fno-elide-constructors01 move

Compare §4: plain return x; cost nothing. Adding std::move cost one move — the std::move turned the return expression into something that is no longer eligible for the named-return optimisation, so the compiler must actually perform a move.

And the compiler says so, at the standing flags:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]

This is worth noting as an exception within this module: most of the failures measured in Modules 10 to 13 produced zero diagnostics. This one is reported without being asked for.

Never write return std::move(localObject); — it is strictly worse than return localObject;.

Chapter 13.9 §9 listed this as the main place not to write std::move; this chapter is the reason.

7. The Fifth Shape

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Obj returnParam(Obj p) { return p; }
Buildcopiesmoves
all three01 move

A parameter cannot be elided, because it does not live in the caller's return slot — it lives in the function's own frame and was constructed by the caller's argument. So returning it requires an actual operation, and since it is a local-like object being returned, that operation is a move.

One move for a composed type is cheap, so this is not a problem. It is worth knowing because it is the one shape where the cost is unavoidable rather than accidental.

8. The Complete Table

Return shapecopiesmovesstatus
return Obj(1);00guaranteed free
Obj x(2); return x;00free in practice; 1 move worst case
return b ? p : q;1 copy0a real copy, always
return std::move(x);01 moveworse than doing nothing; warned
Obj p → return p;01 moveunavoidable, cheap

9. So: Return by Value

The practical conclusion, which is what Chapters 6.3, 10.2 and 13.11 all assumed:

Return by value. Three of the five shapes cost nothing, and the two that cost something cost one move — which for a type following Chapter 13.11's Rule of Zero is a handful of pointer assignments regardless of how much data the object holds.

Prefer returning a temporary where it is natural — guaranteed free.

Returning a named local is fine — free in practice, one move at worst.

Avoid returning a conditional of two named objects if the type is expensive to copy; restructure into two returns.

And never add std::move to a return — the compiler will tell you, and it makes things worse.

The old advice to return via an output parameter, or to allocate and return a pointer, is obsolete for this reason. Chapter 6.9 §5 preferred return values on readability grounds and Chapter 10.1 §7's rule was not to allocate unless forced; this chapter removes the performance argument that used to push the other way.

10. What This Does Not Cover

Two honest boundaries.

Elision also applies in some non-return contexts — notably constructing a parameter from a temporary at a call site, and in throw expressions. The rules are similar in spirit and the return case is where it matters most.

And the standard's own model is not "the copy is removed". In C++17 the guaranteed case is described in terms of the returned expression initializing the caller's object directly, so there never were two objects to collapse. That reformulation is why -fno-elide-constructors cannot affect §3's row, and the precise machinery involves the finer value-category scheme that Chapter 13.6 §8 named and deferred. The measured behaviour is what matters here; the formalism belongs with Module 18's generic-code material, where preserving categories exactly becomes necessary.

11. What to Actually Do

Return by value. Three of five shapes are free and the rest cost one move.

Prefer return T(...) — guaranteed, not optimised.

Return named locals freely — free in practice, one move at worst.

Restructure return b ? p : q; into two returns when the type is expensive.

Never write return std::move(local); — measured worse, and warned.

Keep -Wpessimizing-move enabled — it is in -Wall -Wextra already.

Do not use output parameters to avoid return copies. The reason for that habit is gone.

Do not allocate to avoid returning by value (Chapter 10.1 §7).

And use a counting constructor when you need to know what a particular return actually costs.

12. Mistakes Worth Making Once

Believing a by-value return copies. Measured: three shapes cost 0.

Believing the compiler elides everything. Measured: return b ? p : q; copies at -O2.

Writing return std::move(x);. Measured: 1 move where 0 was available, and the compiler warns.

Using an output parameter for performance. Chapter 6.9 §5's readability cost for no gain.

Returning a pointer to avoid a copy. Chapter 10.1's three limits do not include this.

Expecting -O0 to show the copies. Named-return elision happens at -O0 too.

Expecting -fno-elide-constructors to reveal every elision. It cannot touch the guaranteed case.

Assuming the fallback for a named local is a copy. It is a move.

13. Interview Relevance

"Does returning by value copy?" Not usually, and the strong answer distinguishes: returning a temporary is guaranteed free in C++17, returning a named local is free in practice with a move as the worst case, and a conditional of two named locals genuinely copies.

"What is the difference between guaranteed and permitted elision?" Guaranteed applies to a returned temporary and is not an optimisation — there is no second object in the language's model. Permitted covers the named local. The empirical test is -fno-elide-constructors, which changes only the second — a very strong detail.

"What is wrong with return std::move(x);?" It prevents the named-return optimisation, so it costs a move where nothing was needed, and the compiler warns at -Wall.

"Should you use output parameters to avoid copies?" No — that advice predates C++11 and the measurement contradicts it.

"How would you find out what a specific return costs?" A counting copy and move constructor, built for one run.

14. What You Should Be Able to Say Now

  1. Elision is the removal of a copy by constructing the object in its final location — not an optimisation of a copy.
  2. It is observable, which is why a counting constructor can measure it.
  3. return T(...) is guaranteed free since C++17 — measured 0 copies / 0 moves at -O0, -O2 and under -fno-elide-constructors.
  4. That flag is the empirical test, and it cannot affect the guaranteed case because there is no second object.
  5. return namedLocal; is permitted, not required — measured 0/0 normally and 1 move under the flag.
  6. So its worst case is a move, not a copy.
  7. return b ? p : q; is a real copy at every setting — two candidate objects, and the expression is an lvalue (Chapter 13.6 §5).
  8. Restructuring into two return T(...) statements makes both free.
  9. return std::move(x); costs 1 move where plain return cost 0 — and -Wpessimizing-move is in the standing flags.
  10. A returned parameter cannot be elided — one move, unavoidable and cheap.
  11. Three of five shapes are free; the other two cost one move.
  12. So return by value, and abandon output parameters and returned pointers used for this purpose.
  13. The folklore is wrong in both directions — pessimistic about returns, optimistic about elision.
  14. This is why the Rule of Zero is affordable — composed types are free to hand around.

15. Exercises

Exercise 1 — classify. For each, give the expected copies and moves: return T(1);, T x; return x;, return cond ? a : b;, return std::move(x);, T f(T p) { return p; }.

Exercise 2 — the flag. Explain what -fno-elide-constructors does, which of the above it changes, and why it cannot change the first.

Exercise 3 — restructure it. Rewrite return b ? p : q; so both paths are elided, and say which guarantee you are now relying on.

Exercise 4 — the pessimisation. Explain why return std::move(x); is worse than return x;, quote the warning, and say whether it appears at the standing flags.

Exercise 5 — the old advice. A colleague uses void load(std::vector<Sample>& out); to avoid a copy. Give the measurement that makes the return-by-value version equivalent or better.

Exercise 6 — measure it. Describe the instrument you would add to find out what a specific function's return costs.

Exercise 7 — worst case. For a composed type following the Rule of Zero, say what one move actually costs in operations.

Exercise 8 — both follies. State the pessimistic and optimistic folklore and the measurement that refutes each.

16. Summary

  • Elision removes the copy by building the object in place; it is observable.
  • return T(...) is guaranteed free — 0/0 at -O0, -O2 and -fno-elide-constructors.
  • return namedLocal; is permitted — measured 0/0 normally, 1 move with the flag, so a move is the worst case.
  • return b ? p : q; copies at every setting — two candidates, and an lvalue expression.
  • return std::move(x); costs a move that return x; does not, and -Wpessimizing-move is in the standing flags.
  • A returned parameter costs one move, unavoidably.
  • Three of five shapes are free; return by value.
  • Output parameters and returned pointers for this purpose are obsolete.
  • The folklore errs in both directions — and this is what makes the Rule of Zero affordable.

Thirteen modules of Part I are now complete, and this module ends where Module 10 began. Chapter 10.9 asked which code is responsible for releasing a resource and when, named the question ownership, and could not answer it with the tools available. Module 12 supplied the mechanism — a constructor that cannot be skipped, a destructor that runs on every path — and this module supplied the semantics, so that an owner can be copied, transferred, or forbidden from being either, deliberately and consistently. What remains is that the answer still has to be written out by hand, five functions at a time, for every resource that is not already wrapped. Chapter 13.11 argued that you should almost never do this, and the reason you almost never have to is that the standard library contains types whose entire purpose is to be the owner — one for exclusive ownership, one for shared, and a third for the problem shared ownership creates. The next module is those types, and it opens by treating ownership not as an implementation detail of a class but as something a function signature can state outright.

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.