cs.thefarshad
hard

Move Semantics

Steal resources instead of copying them — lvalues, rvalues, and why moves make modern C++ fast.

When you assign one object to another, C++ traditionally makes a deep copy — duplicating any heap buffer the object owns. For a big std::vector or std::string, that is expensive. Move semantics offer a cheaper path: when the source is about to disappear anyway, just steal its buffer instead of copying it.

The difference comes down to lvalues vs rvalues. An lvalue has a name and persists (x); an rvalue is a temporary with no lasting identity (the result of x + y, or anything you wrap in std::move). Copies happen for lvalues; moves happen for rvalues. Toggle the modes below to compare duplicating a buffer against simply handing over the pointer.

cost: 1 allocation
> Buffer src(4); // owns a heap array
Buffer dst = src; // COPY constructor
// allocate a new array, then duplicate every element
srcstack
ptr = 0x2a00
dststack
ptr = nullptr
Heapbuffers (the expensive part)
0x2a00owner: src
3141
Copy: a second buffer is allocated and filled element by element.
1/4
src constructs a heap array and stores its address. src is the owner of that buffer.

Copy vs move

A copy allocates a new buffer and duplicates every element — O(n). A move copies just the pointer and nulls out the source — O(1), no matter how large the data.

#include <string>
#include <utility>

std::string a = "a very long string ...";

std::string b = a;             // COPY: b gets its own duplicate buffer
                               // a is still valid and unchanged

std::string c = std::move(a);  // MOVE: c steals a's buffer (no allocation)
                               // a is now a valid but empty/unspecified state

After a move, the source object is still valid — you can assign to it or let it be destroyed — but you should not rely on its value.

How a type opts in

A class supports moving by defining a move constructor and move assignment operator. They take an rvalue reference (T&&), grab the source’s resources, and leave the source empty so its destructor does no harm.

class Buffer {
  int* data_;
  size_t n_;
public:
  // Move constructor: steal, do not copy.
  Buffer(Buffer&& other) noexcept
      : data_(other.data_), n_(other.n_) {
      other.data_ = nullptr;   // leave source empty & safe
      other.n_ = 0;
  }
  ~Buffer() { delete[] data_; }  // deleting nullptr is fine
};

Mark move operations noexcept — containers like std::vector only move elements during reallocation if doing so cannot throw; otherwise they fall back to copying.

Takeaways

  • Lvalues (named, persistent) are copied; rvalues (temporaries) can be moved.
  • A copy duplicates the owned buffer (O(n)); a move transfers the pointer and empties the source (O(1)).
  • std::move does not move anything itself — it casts an lvalue to an rvalue so a move can be selected.
  • A moved-from object is valid but unspecified; only destroy it or assign to it.
  • Define move operations as noexcept so containers can use them.