Move Semantics & Smart Pointers

Reviewed & published by Brayan K

By the end of this lesson you'll know exactly why std::move makes code faster, how to write a move constructor and move assignment that steal resources instead of copying them, and how the standard library uses moves under the hood — so your own types stop paying for needless copies.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Imagine moving house. A copy is photocopying every book, re-buying every piece of furniture, and rebuilding it all at the new address — slow and wasteful. A move is loading the moving truck and driving it over: the same stuff arrives at the new place, and the old house is now empty. Nothing was duplicated; ownership of your belongings simply transferred. That is exactly what a move constructor does — it hands the internal buffer to the new object and leaves the old one empty but still safe to clean up.

1. lvalues, rvalues, and T&&

Every expression in C++ is either an lvalue or an rvalue. An lvalue has a name and a lasting address — like a variable x; you can take its address with &x. An rvalue is a temporary with no lasting identity — like the literal 42, or the result of x + 1; it vanishes at the end of the line. This matters because you can safely steal from an rvalue: nothing else will ever look at it again.

A plain reference T& binds to lvalues. The new tool is the rvalue reference, written T&& (two ampersands), which binds to rvalues — to temporaries. That double-ampersand parameter is how a function says "I will only run for throwaway values, so I'm allowed to gut them."

2. std::move and the Move Constructor

std::move sounds like it moves data, but it does nothing at runtime. It is just a cast that turns an lvalue into an rvalue reference, so the compiler picks the move constructor (T(T&&)) instead of the copy constructor (T(const T&)). The move constructor is where the real work happens: it steals the source's internal buffer — usually a single pointer swap — instead of duplicating millions of elements. The source is left empty but valid.

Read this worked example and run it. The class prints whether a copy or a move ran, so you can see exactly which constructor the compiler chose.

#include <iostream>
#include <vector>
#include <string>
using namespace std;

// A class that owns a "big" buffer so copies are visibly expensive.
class Buffer {
    string label;
    vector<int> data;   // imagine this holds millions of ints
public:
    // Normal constructor
    Buffer(string l, int n) : label(l), data(n, 0) {
        cout << label << ": constructed (" << n << " ints)" << endl;
    }

    // Copy constructor — runs when you COPY. It duplicates the whole buffer.
    Buffer(const Buffer& other) : label(other.label), data(other.data) {
        cout << label << ": COPIED (expensive — duplicated the buffer)" << endl;
    }

    // Move constructor — runs when you MOVE. It STEALS the buffer (a pointer
    // swap), leaving 'other' empty but valid. noexcept lets vector use it.
    Buffer(Buffer&& other) noexcept
        : label(move(other.label)), data(move(other.data)) {
        cout << label << ": MOVED (cheap — stole the buffer)" << endl;
    }

    int size() const { return (int)data.size(); }
};

int main() {
    Buffer a("A", 1000000);     // construct A
    cout << "---" << endl;

    Buffer b = a;               // a is an lvalue -> COPY constructor runs
    cout << "b has " << b.size() << " ints; a still has " << a.size() << endl;
    cout << "---" << endl;

    Buffer c = move(a);         // move(a) is an rvalue -> MOVE constructor runs
    cout << "c has " << c.size() << " ints" << endl;
    cout << "a now has " << a.size() << " ints (moved-from = empty)" << endl;

    // Expected output (order):
    //   A: constructed (1000000 ints)
    //   ---
    //   A: COPIED (expensive — duplicated the buffer)
    //   b has 1000000 ints; a still has 1000000
    //   ---
    //   A: MOVED (cheap — stole the buffer)
    //   c has 1000000 ints
    //   a now has 0 ints (moved-from = empty)
    return 0;
}

// ✅ Expected output:
//    A: constructed (1000000 ints)
//    ---
//    A: COPIED (expensive — duplicated the buffer)
//    b has 1000000 ints; a still has 1000000
//    ---
//    A: MOVED (cheap — stole the buffer)
//    c has 1000000 ints
//    a now has 0 ints (moved-from = empty)

Your turn. The class below has a working copy constructor; the move constructor is missing two pieces. Fill in the ___ blanks so it steals the buffer and promises not to throw.

#include <iostream>
#include <vector>
#include <string>
using namespace std;

class Box {
    vector<int> data;
public:
    Box(int n) : data(n, 1) {}

    // Copy constructor — already written for you.
    Box(const Box& other) : data(other.data) {
        cout << "COPIED" << endl;
    }

    // 🎯 YOUR TURN — finish the MOVE constructor.
    // Steal 'other.data' instead of copying it, and don't throw.
    Box(Box&& other) ___ : data(___) {   // 👉 add 'noexcept', then move(other.data)
        cout << "MOVED" << endl;
    }

    int size() const { return (int)data.size(); }
};

int main() {
    Box a(5);
    Box b = move(a);            // should trigger your MOVE constructor
    cout << "b size: " << b.size() << endl;

    // ✅ Expected output:
    //    MOVED
    //    b size: 5
    return 0;
}

Now practise calling std::move yourself. Moving a std::string hands its character buffer to the destination and leaves the original empty — no characters are copied.

#include <iostream>
#include <string>
#include <utility>   // std::move lives here
using namespace std;

int main() {
    // 🎯 YOUR TURN — move a string instead of copying it.
    string from = "a very long sentence pretending to be huge";
    string to;

    // 1) Move 'from' into 'to' (steal its characters, don't copy them)
    to = ___;                   // 👉 move(from)

    cout << "to   = \"" << to << "\"" << endl;
    cout << "from = \"" << from << "\" (moved-from: empty but valid)" << endl;

    // ✅ Expected output:
    //    to   = "a very long sentence pretending to be huge"
    //    from = "" (moved-from: empty but valid)
    return 0;
}

🔎 Deep Dive: why moving is cheap

A std::string or std::vector is a small handle (a pointer to a heap buffer, plus a size and capacity). Copying allocates a brand-new buffer and copies every byte — O(n). Moving copies just the pointer, size, and capacity, then zeroes out the source so it doesn't free the buffer you stole — O(1), no matter how big the data is.

string a = "Hello, this is a long string";
string b = move(a);   // b steals a's buffer (pointer swap)
// b == "Hello, this is a long string"
// a == ""  (valid but empty — its buffer now belongs to b)

That is the whole point: a move transfers ownership of the existing resource instead of building a second copy of it.

3. Move Assignment and the Rule of Five

A move constructor builds a new object from a temporary. Move assignment (operator=(T&&)) instead replaces an existing object: it must first free whatever it already holds, then steal the source's resource. The Rule of Five says that once you manually manage a resource and write any one of these five special members, you should write all five so they agree on ownership:

The five special members

Destructor, copy constructor, copy assignment, move constructor, move assignment. Notice the move assignment guards against self-move with if (this != &o) before it deletes anything — without that check, assigning an object to itself would free its own buffer first.

class Widget {
    int* data;
public:
    Widget(int n) : data(new int[n]) {}          // 1. constructor

    ~Widget() { delete[] data; }                 // 2. destructor

    Widget(const Widget& o);                      // 3. copy constructor
    Widget& operator=(const Widget& o);           // 4. copy assignment

    Widget(Widget&& o) noexcept                   // 5a. move constructor
        : data(o.data) { o.data = nullptr; }
    Widget& operator=(Widget&& o) noexcept {      // 5b. move assignment
        if (this != &o) {                         //     guard against self-move
            delete[] data;                        //     free what we hold
            data = o.data;                        //     steal o's pointer
            o.data = nullptr;                     //     leave o safe to destroy
        }
        return *this;
    }
};

In real code you'd usually let members like std::vector manage the memory so the compiler generates all five correctly for you. Write them by hand only when you own a raw resource.

4. How std::vector Uses Moves

When a std::vector runs out of capacity, it allocates a bigger buffer and transfers the existing elements into it. If your type's move constructor is marked noexcept, the vector moves each element (cheap). If it isn't, the vector copies them instead — it needs the strong exception guarantee, and a move that might throw could leave it in a broken state. That one keyword is the difference between fast and slow growth.

#include <iostream>
#include <vector>
#include <string>
using namespace std;

struct Item {
    string name;
    Item(string n) : name(n) { cout << name << ": built" << endl; }
    Item(const Item& o) : name(o.name) { cout << name << ": COPIED" << endl; }
    Item(Item&& o) noexcept : name(move(o.name)) { cout << "moved" << endl; }
};

int main() {
    vector<Item> v;
    v.reserve(2);               // room for 2 — no reallocation yet
    cout << "push #1" << endl;  v.push_back(Item("one"));
    cout << "push #2" << endl;  v.push_back(Item("two"));

    cout << "push #3 (capacity exceeded -> reallocate)" << endl;
    v.push_back(Item("three")); // vector grows; existing items are MOVED
    // Because Item's move ctor is noexcept, the vector MOVES the old items
    // into the bigger buffer instead of COPYING them. Drop 'noexcept' and
    // the vector falls back to copying for safety.
    return 0;
}

// ✅ Expected output:
//    push #1
//    one: built
//    moved
//    push #2
//    two: built
//    moved
//    push #3 (capacity exceeded -> reallocate)
//    three: built
//    moved
//    moved
//    moved

Common Errors (and the fix)

📋 Quick Reference

ConceptSyntaxMeaning
lvalue referenceT& r = x;Binds to a named object
rvalue referenceT&& r = T();Binds to a temporary
Cast to rvaluestd::move(x)Enables a move (no runtime cost)
Move constructorT(T&&) noexceptBuild by stealing resources
Move assignmentT& operator=(T&&)Replace by stealing resources
Rule of Five~T, copy x2, move x2Define all five together

Mini-Challenge: a move-aware Document

No blanks this time — just a brief and an outline. Build a class with both a copy and a move constructor, then prove which one runs by copying once and moving once. Check your output against the comments.

#include <iostream>
#include <vector>
#include <string>
using namespace std;

// 🎯 MINI-CHALLENGE: a move-aware "Document" class
// 1. Give it a private  vector<string> lines;  and a  string title;
// 2. Constructor: Document(string t) sets title and prints "<t>: created".
// 3. Add a COPY constructor that prints "<title>: copied".
// 4. Add a MOVE constructor (mark it noexcept) that move()s both members
//    and prints "<title>: moved".
// 5. In main(): build d1, do  Document d2 = d1;        // should print "copied"
//                       then  Document d3 = move(d1);  // should print "moved"
//
// ✅ Expected output:
//    Report: created
//    Report: copied
//    Report: moved

class Document {
    // your code here
};

int main() {
    // your code here
    return 0;
}

Pro Tips

🎉 Lesson Complete

Practice quiz

What is the difference between an lvalue and an rvalue?

  • An lvalue is always const; an rvalue is mutable
  • An lvalue is on the heap; an rvalue is on the stack
  • An lvalue has a name and a stable address; an rvalue is a temporary with no lasting identity
  • There is no difference

Answer: An lvalue has a name and a stable address; an rvalue is a temporary with no lasting identity. An lvalue (like a variable x) has a name and address; an rvalue (like 42 or x+1) is a throwaway temporary you can steal from.

What does std::move actually do at run time?

  • Nothing at run time — it is just a cast to an rvalue reference
  • It copies the data to a new location
  • It frees the source object
  • It allocates new memory

Answer: Nothing at run time — it is just a cast to an rvalue reference. std::move moves nothing; it casts an lvalue to an rvalue reference so the compiler picks the move constructor/assignment.

Which reference type binds to a temporary (rvalue)?

  • int&
  • const int*
  • int*&
  • int&&

Answer: int&&. int&& is an rvalue reference and binds to temporaries; int& binds to named lvalues.

After string b = std::move(a); for a std::string a, what is a's state?

  • a still holds its original characters
  • a is valid but unspecified — for string typically empty
  • a is destroyed and unusable
  • a now points to b

Answer: a is valid but unspecified — for string typically empty. A moved-from object is valid but unspecified; std::string is typically left empty. Don't rely on its value.

Why does moving a std::vector cost O(1) while copying costs O(n)?

  • Moving copies just the pointer, size, and capacity, then zeroes the source
  • Moving compresses the data
  • Copying skips the elements
  • Moving uses a faster memcpy of every element

Answer: Moving copies just the pointer, size, and capacity, then zeroes the source. A move steals the heap buffer by copying the small handle (pointer/size/capacity); copying duplicates every element.

Why must a move constructor be marked noexcept?

  • It is required for the class to compile
  • It makes the move faster by skipping checks
  • So std::vector moves (rather than copies) elements when it reallocates
  • It prevents the destructor from running

Answer: So std::vector moves (rather than copies) elements when it reallocates. std::vector only uses your move ctor on reallocation if it promises not to throw; without noexcept it copies for the strong guarantee.

What are the five special members in the Rule of Five?

  • Constructor, destructor, copy ctor, copy assignment, swap
  • Destructor, copy ctor, copy assignment, move ctor, move assignment
  • Constructor, destructor, operator+, operator==, operator<<
  • Two constructors and three destructors

Answer: Destructor, copy ctor, copy assignment, move ctor, move assignment. The Rule of Five: destructor, copy constructor, copy assignment, move constructor, and move assignment all go together.

Why does move assignment guard with if (this != &o) before deleting?

  • To skip the work when objects are equal in value
  • To make the operation noexcept
  • It is purely stylistic
  • To avoid freeing its own buffer on a self-move like x = std::move(x);

Answer: To avoid freeing its own buffer on a self-move like x = std::move(x);. Without the self-move guard, deleting before stealing would free the very buffer it is about to take.

What happens if you call std::move on a const object, like move(c) where c is const?

  • It moves as usual
  • It produces a const T&&, which binds to the copy constructor — you silently get a copy
  • It is a compile error
  • It throws at run time

Answer: It produces a const T&&, which binds to the copy constructor — you silently get a copy. move(const T) yields const T&&, which can't bind to a non-const move ctor, so the copy constructor runs instead.

For a local variable, what should you write to return it most efficiently?

  • return std::move(local);
  • return &local;
  • return local; — letting the compiler elide (RVO)
  • return *local;

Answer: return local; — letting the compiler elide (RVO). return local; lets the compiler apply copy elision/RVO; return std::move(local); actually disables that optimisation.

Continue this course

Frequently asked questions

What is the difference between an lvalue and an rvalue?

An lvalue has a name and a stable address you can take with & — like a variable x. An rvalue is a temporary with no lasting identity — like the result of x + 1 or a literal. Moving is safe from rvalues because nobody else will use them afterwards.

What does std::move actually do?

Nothing at runtime — it does not move anything. std::move is just a cast that turns an lvalue into an rvalue reference, which makes the compiler pick the move constructor or move assignment instead of the copy versions. The actual stealing happens inside those move operations.

Why must a move constructor be noexcept?

std::vector only uses your move constructor during reallocation if it promises not to throw. Without noexcept, the vector copies every element instead (for the strong exception guarantee), silently throwing away the performance you wrote the move constructor to get.

Is it safe to use an object after I move from it?

You may assign to it or destroy it, but you must not assume anything about its value — a moved-from object is valid but unspecified. For standard types like string and vector it is typically left empty, but never rely on that across all types.

What is the Rule of Five?

If your class manually manages a resource and you define any one of the destructor, copy constructor, copy assignment, move constructor, or move assignment, you almost certainly need to define all five — they have to agree on how the resource is owned, copied, moved, and freed.