Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Move Semantics & Ownership
C++

Move Semantics & Ownership

Practice 15 questions covering move semantics, copy and move operations, ownership, rvalue references, and resource management.

1. What is the precise effect of invoking std::move(x) on an object x in C++?

Answer: std::move(x) is purely a static cast of x to an rvalue reference — it performs no runtime resource transfer by itself.

The most important thing to understand is that std::move does nothing at runtime. It doesn’t free memory, doesn’t copy data, doesn’t invalidate the object. It is a cast: static_cast<std::remove_reference_t<T>&&>(x). That’s all.

Its purpose is to change the value category of the expression x from lvalue to rvalue (specifically an xvalue). That change matters only because of how overload resolution works: when you pass the result to a constructor or assignment operator, the compiler now prefers the overload that takes an rvalue reference — i.e., the move constructor or move assignment — instead of the copy.

So the actual transfer of resources (stealing a buffer pointer, moving heap data) happens later, in the move constructor or move assignment the value is passed into. std::move merely enables that choice.

The common misconception is “std::move moves data.” It doesn’t — it labels the value so that other code will move it. The interview answer: std::move is an unconditional rvalue cast with zero runtime effect; resource transfer occurs only when the result feeds a move constructor or move assignment.

Answer:

std::move(x) is purely a static cast of x to an rvalue reference — it performs no runtime resource transfer by itself.

The most important thing to understand is that std::move does nothing at runtime. It doesn’t free memory, doesn’t copy data, doesn’t invalidate the object. It is a cast: static_cast<std::remove_reference_t<T>&&>(x). That’s all.

Its purpose is to change the value category of the expression x from lvalue to rvalue (specifically an xvalue). That change matters only because of how overload resolution works: when you pass the result to a constructor or assignment operator, the compiler now prefers the overload that takes an rvalue reference — i.e., the move constructor or move assignment — instead of the copy.

So the actual transfer of resources (stealing a buffer pointer, moving heap data) happens later, in the move constructor or move assignment the value is passed into. std::move merely enables that choice.

The common misconception is “std::move moves data.” It doesn’t — it labels the value so that other code will move it. The interview answer: std::move is an unconditional rvalue cast with zero runtime effect; resource transfer occurs only when the result feeds a move constructor or move assignment.

2. What is the fundamental operational difference between std::move and std::forward in template metaprogramming?

Answer: std::move unconditionally casts its argument to an rvalue reference. std::forward<T> conditionally casts — it preserves the original value category of the argument passed to a forwarding (universal) reference.

Both are casts (no runtime work), but they answer different questions.

std::move(x) — “I don’t care about x anymore; treat it as an rvalue always.” Unconditional.

std::forward<T>(arg) — used with a forwarding reference T&& in a template. T is deduced to be T& if the caller passed an lvalue, or T if they passed an rvalue. forward then casts the argument back to that original category: lvalue stays lvalue, rvalue stays rvalue. This is “perfect forwarding” — the template forwards parameters to another function while preserving exactly how they were passed.

The rule of thumb: use move when you own the value and want to move it; use forward when forwarding a parameter through a template. The interview answer: move is unconditional, forward is conditional and preserves the original value category through forwarding references.

Answer:

std::move unconditionally casts its argument to an rvalue reference. std::forward<T> conditionally casts — it preserves the original value category of the argument passed to a forwarding (universal) reference.

Both are casts (no runtime work), but they answer different questions.

std::move(x) — “I don’t care about x anymore; treat it as an rvalue always.” Unconditional.

std::forward<T>(arg) — used with a forwarding reference T&& in a template. T is deduced to be T& if the caller passed an lvalue, or T if they passed an rvalue. forward then casts the argument back to that original category: lvalue stays lvalue, rvalue stays rvalue. This is “perfect forwarding” — the template forwards parameters to another function while preserving exactly how they were passed.

The rule of thumb: use move when you own the value and want to move it; use forward when forwarding a parameter through a template. The interview answer: move is unconditional, forward is conditional and preserves the original value category through forwarding references.

3. What does the “Rule of Zero” advocate in modern C++ object design?

Answer: Classes that don’t directly manage raw resources should declare no special member functions at all — letting compiler-generated defaults handle everything, because the class owns its resources via RAII types like std::unique_ptr, std::vector, and std::string.

The classical “Rule of Three” said: if you define a destructor, you almost certainly need the copy constructor and copy assignment too. The “Rule of Five” (C++11) added move operations. The Rule of Zero observes that you usually shouldn’t define any of them.

If a class’s members are all RAII types — smart pointers, containers, standard strings — those members already know how to copy, move, and destroy correctly. The compiler-generated copy/move/destructor simply delegate to them, and they work flawlessly. Writing your own destructor (or any special function) is unnecessary code, and worse, declaring certain special members suppresses others and can silently change behavior.

The goal is: no hand-written special members, no raw new/delete, no manual resource handling. Resources are managed by library types, and the class gets correct behavior for free. (The “Rule of Five” then only applies to the rare class that genuinely manages raw resources.) The interview answer: the Rule of Zero says avoid writing destructors/copy/move functions; manage resources through RAII types so compiler defaults are correct.

Answer:

Classes that don’t directly manage raw resources should declare no special member functions at all — letting compiler-generated defaults handle everything, because the class owns its resources via RAII types like std::unique_ptr, std::vector, and std::string.

The classical “Rule of Three” said: if you define a destructor, you almost certainly need the copy constructor and copy assignment too. The “Rule of Five” (C++11) added move operations. The Rule of Zero observes that you usually shouldn’t define any of them.

If a class’s members are all RAII types — smart pointers, containers, standard strings — those members already know how to copy, move, and destroy correctly. The compiler-generated copy/move/destructor simply delegate to them, and they work flawlessly. Writing your own destructor (or any special function) is unnecessary code, and worse, declaring certain special members suppresses others and can silently change behavior.

The goal is: no hand-written special members, no raw new/delete, no manual resource handling. Resources are managed by library types, and the class gets correct behavior for free. (The “Rule of Five” then only applies to the rare class that genuinely manages raw resources.) The interview answer: the Rule of Zero says avoid writing destructors/copy/move functions; manage resources through RAII types so compiler defaults are correct.

4. What safety risk occurs when a lambda captures a local variable by reference ([&var])?

Answer: If the lambda outlives the variable’s scope, invoking it later reads a dangling reference — undefined behavior.

Capturing by reference does not extend the variable’s lifetime. It stores a reference to the variable. As long as the lambda is used within the variable’s lifetime, all is well. But the moment the lambda escapes that scope — returned from the function, stored in a container that outlives it, dispatched to an async thread — the reference dangles: it points at memory that’s already been destroyed.

auto makeLambda() {
    int x = 42;
    return [&x] { return x; };  // dangling when called later
}

The fix options:

  • Capture by value ([x]) — the closure owns a copy, safe to escape.
  • Capture by reference only when you’re certain the lambda is used strictly inside the variable’s scope.

This is the same lifetime hazard as a raw reference or pointer to a local — lambdas just make it easy to write by accident, because the escape is often indirect (passing a lambda to a thread or a callback). The interview answer: [&var] captures a reference that dangles if the lambda outlives var’s scope — UB on use.

Answer:

If the lambda outlives the variable’s scope, invoking it later reads a dangling reference — undefined behavior.

Capturing by reference does not extend the variable’s lifetime. It stores a reference to the variable. As long as the lambda is used within the variable’s lifetime, all is well. But the moment the lambda escapes that scope — returned from the function, stored in a container that outlives it, dispatched to an async thread — the reference dangles: it points at memory that’s already been destroyed.

auto makeLambda() {
    int x = 42;
    return [&x] { return x; };  // dangling when called later
}

The fix options:

  • Capture by value ([x]) — the closure owns a copy, safe to escape.
  • Capture by reference only when you’re certain the lambda is used strictly inside the variable’s scope.

This is the same lifetime hazard as a raw reference or pointer to a local — lambdas just make it easy to write by accident, because the escape is often indirect (passing a lambda to a thread or a callback). The interview answer: [&var] captures a reference that dangles if the lambda outlives var’s scope — UB on use.

5. What will be the output of the following code regarding std::unique_ptr move mechanics?

std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2 = std::move(p1);
if (!p1) {
    std::cout << "p1 is null, ";
}
std::cout << *p2;

Output: p1 is null, 42.

std::unique_ptr is a move-only type — it owns its resource exclusively and cannot be copied. Moving it (std::move(p1)) transfers ownership to the target:

  • p2 now owns the int with value 42.
  • p1 is left in the valid-but-empty state: it holds nullptr.

So the check !p1 is true (it’s null), printing p1 is null, . Then *p2 safely dereferences the owned integer, printing 42. Output: p1 is null, 42.

The key behavior: moving a unique_ptr never copies the pointee; it just hands the raw pointer over and nulls the source. The interview answer: p1 is null, 42.

Answer:

p1 is null, 42.

std::unique_ptr is a move-only type — it owns its resource exclusively and cannot be copied. Moving it (std::move(p1)) transfers ownership to the target:

  • p2 now owns the int with value 42.
  • p1 is left in the valid-but-empty state: it holds nullptr.

So the check !p1 is true (it’s null), printing p1 is null, . Then *p2 safely dereferences the owned integer, printing 42. Output: p1 is null, 42.

The key behavior: moving a unique_ptr never copies the pointee; it just hands the raw pointer over and nulls the source. The interview answer: p1 is null, 42.

6. What is the fundamental issue with returning a reference to a local variable from a function?

Answer: The local variable is destroyed when the function’s stack frame pops, so the caller holds a dangling reference — dereferencing it is undefined behavior.

int& bad() {
    int x = 42;
    return x;   // x dies here
}

Local variables have automatic storage duration — they’re destroyed at the end of the scope where they’re declared. When bad() returns, x no longer exists, but the caller received a reference to where it used to be. Accessing that reference later reads freed stack memory: a dangling reference, which is UB.

(Even reading it once often “appears” to work because the stack memory hasn’t been overwritten — which makes the bug dangerous: it works in tests and corrupts mysteriously in production.)

The fixes:

  • Return by value — the value is copied/moved out (and thanks to copy elision, often no copy happens at all).
  • If you must return a reference, return one to an object with longer lifetime — a static, a class member, or a caller-supplied object.

The interview answer: returning a reference to a local yields a dangling reference — UB on access, because the local is destroyed when the frame exits.

Answer:

The local variable is destroyed when the function’s stack frame pops, so the caller holds a dangling reference — dereferencing it is undefined behavior.

int& bad() {
    int x = 42;
    return x;   // x dies here
}

Local variables have automatic storage duration — they’re destroyed at the end of the scope where they’re declared. When bad() returns, x no longer exists, but the caller received a reference to where it used to be. Accessing that reference later reads freed stack memory: a dangling reference, which is UB.

(Even reading it once often “appears” to work because the stack memory hasn’t been overwritten — which makes the bug dangerous: it works in tests and corrupts mysteriously in production.)

The fixes:

  • Return by value — the value is copied/moved out (and thanks to copy elision, often no copy happens at all).
  • If you must return a reference, return one to an object with longer lifetime — a static, a class member, or a caller-supplied object.

The interview answer: returning a reference to a local yields a dangling reference — UB on access, because the local is destroyed when the frame exits.

7. What causes undefined behavior in this string initialization statement?

const char* ptr = "Hello, World!";
char* str = const_cast<char*>(ptr);
str[0] = 'h';

Answer: String literals may live in read-only memory; modifying them — even after const_cast removes const — is undefined behavior.

String literals are stored in read-only memory (in typical implementations). The const char* declaration is not just a type qualifier — it reflects that the object genuinely cannot be modified.

const_cast removes the const qualifier from the pointer type, so char* str = const_cast<char*>(ptr) compiles and str[0] = 'h' is legal syntax. But it doesn’t change where the literal lives. Writing through str attempts to modify read-only memory: a crash (access violation) in practice, and undefined behavior per the standard.

The rules to remember:

  • const_cast is for removing const from objects you know are actually mutable (e.g., a const reference to a non-const variable).
  • Casting away const from a truly const object and modifying it is UB.

The interview answer: writing to a string literal through a const_cast-stripped pointer is UB — the literal is in read-only memory.

Answer:

String literals may live in read-only memory; modifying them — even after const_cast removes const — is undefined behavior.

String literals are stored in read-only memory (in typical implementations). The const char* declaration is not just a type qualifier — it reflects that the object genuinely cannot be modified.

const_cast removes the const qualifier from the pointer type, so char* str = const_cast<char*>(ptr) compiles and str[0] = 'h' is legal syntax. But it doesn’t change where the literal lives. Writing through str attempts to modify read-only memory: a crash (access violation) in practice, and undefined behavior per the standard.

The rules to remember:

  • const_cast is for removing const from objects you know are actually mutable (e.g., a const reference to a non-const variable).
  • Casting away const from a truly const object and modifying it is UB.

The interview answer: writing to a string literal through a const_cast-stripped pointer is UB — the literal is in read-only memory.

8. What compile-time optimization is enabled by Return Value Optimization (RVO / NRVO)?

Answer: The compiler constructs the returned object directly in the caller’s target memory, eliminating the copy/move constructor call entirely.

Normally, returning a value by value would be: function builds a local T, copies/moves it into a temporary, then copies/moves that into the caller’s variable. RVO (Return Value Optimization) lets the compiler skip the temporary and the copies: the function constructs the result directly into the address where the caller expects the final value.

T make() {
    return T{...};   // constructed directly in caller's variable
}
T t = make();        // NO copy/move happens

NRVO (Named RVO) extends this to named locals: return local; where local is a local variable — the compiler can still elide the copy in many cases.

The upshot: returning big objects by value is often free — no copy, no move, just direct construction. This is why “return by value” is the idiomatic, efficient choice in modern C++.

The interview answer: RVO/NRVO constructs the return value in place in the caller’s memory, eliminating copy/move constructor calls.

Answer:

The compiler constructs the returned object directly in the caller’s target memory, eliminating the copy/move constructor call entirely.

Normally, returning a value by value would be: function builds a local T, copies/moves it into a temporary, then copies/moves that into the caller’s variable. RVO (Return Value Optimization) lets the compiler skip the temporary and the copies: the function constructs the result directly into the address where the caller expects the final value.

T make() {
    return T{...};   // constructed directly in caller's variable
}
T t = make();        // NO copy/move happens

NRVO (Named RVO) extends this to named locals: return local; where local is a local variable — the compiler can still elide the copy in many cases.

The upshot: returning big objects by value is often free — no copy, no move, just direct construction. This is why “return by value” is the idiomatic, efficient choice in modern C++.

The interview answer: RVO/NRVO constructs the return value in place in the caller’s memory, eliminating copy/move constructor calls.

9. What is the effect of applying const to a function parameter passed by value (void foo(const int x))?

Answer: It prevents x from being modified inside the function body, with no effect on how callers pass arguments.

By-value parameters receive a copy of the argument. The const here qualifies the local copy, not the caller’s original. The effects:

  • Inside foo, x is read-only — attempting x = 42; is a compile error.
  • Callers are unaffected: foo(5) and foo(variable) both compile exactly as they would without const. The caller’s variable is never modified either way (it was passed by value).

So const int x is purely a self-documentation and safety device for the function implementer — it signals “this parameter is an input only, I won’t mutate my local copy.” It does not change the function signature’s call syntax.

The interview answer: const on a value parameter restricts modification of the local copy inside the body; call sites are unchanged.

Answer:

It prevents x from being modified inside the function body, with no effect on how callers pass arguments.

By-value parameters receive a copy of the argument. The const here qualifies the local copy, not the caller’s original. The effects:

  • Inside foo, x is read-only — attempting x = 42; is a compile error.
  • Callers are unaffected: foo(5) and foo(variable) both compile exactly as they would without const. The caller’s variable is never modified either way (it was passed by value).

So const int x is purely a self-documentation and safety device for the function implementer — it signals “this parameter is an input only, I won’t mutate my local copy.” It does not change the function signature’s call syntax.

The interview answer: const on a value parameter restricts modification of the local copy inside the body; call sites are unchanged.

10. What is the evaluation order guarantee of function arguments in C++17?

Answer: Arguments are evaluated in un-interleaved but unspecified order — each argument is fully evaluated before the next begins, but the compiler may pick any order.

Pre-C++17, argument evaluation was completely unspecified and could be interleaved in pathological cases. C++17 tightened this: the evaluations of individual arguments can no longer be interleaved. So for f(a(), b(), c()):

  • a(), b(), c() each complete fully before the next starts — no partial interleaving.
  • But the order among them — a then b then c, or c then b then a — is still unspecified.

This matters for correctness: if the argument expressions have side effects or read/write shared state, you must not rely on any particular order. f(++i, i++) is still a bug. The guarantee just means each individual subexpression’s evaluation is atomic with respect to the others.

The interview answer: arguments are evaluated completely (non-interleaved), but their relative order remains unspecified.

Answer:

Arguments are evaluated in un-interleaved but unspecified order — each argument is fully evaluated before the next begins, but the compiler may pick any order.

Pre-C++17, argument evaluation was completely unspecified and could be interleaved in pathological cases. C++17 tightened this: the evaluations of individual arguments can no longer be interleaved. So for f(a(), b(), c()):

  • a(), b(), c() each complete fully before the next starts — no partial interleaving.
  • But the order among them — a then b then c, or c then b then a — is still unspecified.

This matters for correctness: if the argument expressions have side effects or read/write shared state, you must not rely on any particular order. f(++i, i++) is still a bug. The guarantee just means each individual subexpression’s evaluation is atomic with respect to the others.

The interview answer: arguments are evaluated completely (non-interleaved), but their relative order remains unspecified.

11. What is the lifetime of a temporary object bound to a const lvalue reference?

const std::string& ref = std::string("Hello");

Answer: The temporary’s lifetime is extended to match the reference’s lifetime.

Normally a temporary is destroyed at the end of the full expression. But binding a temporary to a const lvalue reference (or an rvalue reference) triggers lifetime extension: the temporary lives as long as the reference variable does. So ref safely refers to the string for its whole lifetime — no dangling.

const std::string& ref = std::string("Hello");
// ref is valid here, the string is still alive

Note the rule: the reference must be a local reference variable (not a reference member of a class — those don’t extend). The interview answer: binding a temporary to a local const lvalue reference or rvalue reference extends the temporary’s lifetime to match the reference variable’s scope.

Answer:

The temporary’s lifetime is extended to match the reference’s lifetime.

Normally a temporary is destroyed at the end of the full expression. But binding a temporary to a const lvalue reference (or an rvalue reference) triggers lifetime extension: the temporary lives as long as the reference variable does. So ref safely refers to the string for its whole lifetime — no dangling.

const std::string& ref = std::string("Hello");
// ref is valid here, the string is still alive

Note the rule: the reference must be a local reference variable (not a reference member of a class — those don’t extend). The interview answer: binding a temporary to a local const lvalue reference or rvalue reference extends the temporary’s lifetime to match the reference variable’s scope.

12. What is the fundamental requirement for using std::string_view safely?

Answer: The underlying character array it references must remain valid and outlive the string_view.

std::string_view is a non-owning view — just a pointer plus a length over somebody else’s character buffer. It never copies or owns the data. That’s its performance appeal (zero-copy string handling) and its danger:

std::string_view v = getString().c_str();  // getString() destroyed → v dangles

If the backing string is destroyed or modified (e.g., a std::string reallocates), the view becomes a dangling pointer, and reading through it is undefined behavior. So the rule is: the view is only valid as long as the underlying buffer lives. No null-termination is required (it carries its own length), and it’s meant to be cheap to pass by value. The interview answer: the referenced character array must outlive the string_view — it owns nothing.

Answer:

The underlying character array it references must remain valid and outlive the string_view.

std::string_view is a non-owning view — just a pointer plus a length over somebody else’s character buffer. It never copies or owns the data. That’s its performance appeal (zero-copy string handling) and its danger:

std::string_view v = getString().c_str();  // getString() destroyed → v dangles

If the backing string is destroyed or modified (e.g., a std::string reallocates), the view becomes a dangling pointer, and reading through it is undefined behavior. So the rule is: the view is only valid as long as the underlying buffer lives. No null-termination is required (it carries its own length), and it’s meant to be cheap to pass by value. The interview answer: the referenced character array must outlive the string_view — it owns nothing.

13. What is the execution result of the following pointer arithmetic operation on a 64-bit platform?

int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr;
ptr = ptr + 2;
std::cout << *ptr;

Output: 30.

Pointer arithmetic is scaled by the size of the pointed-to type. ptr + 2 on an int* advances the address by 2 * sizeof(int) — i.e., two array elements, not two bytes. ptr starts at arr[0] (10); ptr + 2 points at arr[2], which is 30. Output: 30.

Answer:

30.

Pointer arithmetic is scaled by the size of the pointed-to type. ptr + 2 on an int* advances the address by 2 * sizeof(int) — i.e., two array elements, not two bytes. ptr starts at arr[0] (10); ptr + 2 points at arr[2], which is 30. Output: 30.

14. What design bug is prevented by checking this != &rhs inside custom copy assignment operators?

Answer: Self-assignment — where freeing this’s resources destroys the source (rhs) data before it’s copied, causing memory corruption.

Copy assignment typically: delete existing resources, then copy rhs’s resources. If x = x; (self-assignment) and there’s no identity check:

  • this and &rhs are the same object.
  • Deleting this’s internal buffers also destroys the very data you’re about to copy from rhs.
  • Copying from freed memory → garbage state or crash → undefined behavior.

The guard if (this != &rhs) { ... } skips the whole operation on self-assignment. (Modern alternative: copy-and-swap idiom — copy into a temporary, then swap — which is naturally self-assignment-safe.) The interview answer: it prevents self-assignment, where freeing this’s resources would destroy rhs (the same object) before the copy happens.

Answer:

Self-assignment — where freeing this’s resources destroys the source (rhs) data before it’s copied, causing memory corruption.

Copy assignment typically: delete existing resources, then copy rhs’s resources. If x = x; (self-assignment) and there’s no identity check:

  • this and &rhs are the same object.
  • Deleting this’s internal buffers also destroys the very data you’re about to copy from rhs.
  • Copying from freed memory → garbage state or crash → undefined behavior.

The guard if (this != &rhs) { ... } skips the whole operation on self-assignment. (Modern alternative: copy-and-swap idiom — copy into a temporary, then swap — which is naturally self-assignment-safe.) The interview answer: it prevents self-assignment, where freeing this’s resources would destroy rhs (the same object) before the copy happens.

15. What problem is solved by using std::move_iterator?

Answer: It converts dereference results from lvalues into rvalue references, so algorithms and containers move elements instead of copying them.

std::move_iterator wraps an ordinary iterator. Dereferencing it yields T&& (an xvalue) instead of T&. That changes how consuming code behaves — std::copy or a range constructor will move the elements into the destination rather than copy:

std::vector<std::string> src = {"a", "b", "c"};
std::vector<std::string> dst(std::make_move_iterator(src.begin()),
                            std::make_move_iterator(src.end()));
// elements moved (stolen) from src, not copied

Great for efficiently transferring ownership of heap-owning elements from one container to another. The interview answer: dereferencing yields rvalue references, letting algorithms/constructors move elements instead of copying.

Answer:

It converts dereference results from lvalues into rvalue references, so algorithms and containers move elements instead of copying them.

std::move_iterator wraps an ordinary iterator. Dereferencing it yields T&& (an xvalue) instead of T&. That changes how consuming code behaves — std::copy or a range constructor will move the elements into the destination rather than copy:

std::vector<std::string> src = {"a", "b", "c"};
std::vector<std::string> dst(std::make_move_iterator(src.begin()),
                            std::make_move_iterator(src.end()));
// elements moved (stolen) from src, not copied

Great for efficiently transferring ownership of heap-owning elements from one container to another. The interview answer: dereferencing yields rvalue references, letting algorithms/constructors move elements instead of copying.

My Private Notes

Notes are auto-saved locally to this device.