Skip to main content
    Courses/C++/Move Semantics & Smart Pointers

    Lesson 14 • Expert

    Move Semantics in C++

    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.

    What You'll Learn

    • Tell lvalues apart from rvalues, and read an rvalue reference (T&&)
    • Write a move constructor and move assignment that transfer ownership
    • Use std::move to turn a copy into a cheap move
    • Explain why moving avoids the cost of duplicating big buffers
    • Apply the Rule of Five so all your special members agree
    • See how std::vector moves elements when it reallocates

    💡 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."

    int x = 10;        // x is an lvalue (has a name & address)
    int& lref = x;     // T&  binds to the lvalue x       -> OK
    int&& rref = 10;   // T&& binds to the rvalue 10      -> OK
    // int&& bad = x;  // ERROR: x is an lvalue, not a temporary

    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.

    Worked example: copy vs move (it prints which one runs)

    Run it and watch COPIED vs MOVED. After the move, the source is emptied.

    Try it Yourself »
    C++
    #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
    ...

    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.

    🎯 Your turn: finish the move constructor

    Add noexcept and move() the member, then check the output prints MOVED.

    Try it Yourself »
    C++
    #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)
           
    ...

    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.

    🎯 Your turn: call std::move

    Move 'from' into 'to' and confirm 'from' ends up empty but valid.

    Try it Yourself »
    C++
    #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)" << e
    ...

    🔎 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.

    Worked example: vector reallocation moves elements

    Watch what happens on the 3rd push_back when capacity is exceeded.

    Try it Yourself »
    C++
    #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"));
       
    ...

    Common Errors (and the fix)

    • Using a moved-from object: after auto b = move(a);, reading a's value is a bug — it is valid but unspecified. Only assign to it or let it be destroyed.
    • Missing noexcept on the move constructor: vector silently falls back to copying during reallocation, so your "fast" type quietly runs slow. Always write Buffer(Buffer&&) noexcept.
    • Self-move without a guard: in move assignment, delete-ing before checking if (this != &o) frees the very buffer you're about to steal. Guard against x = move(x);.
    • Moving a const object: move(c) on a const T c; produces a const T&&, which binds to the copy constructor — you silently get a copy, not a move. Don't mark movable values const.
    • Returning move(local): return move(x); for a local actually disables copy elision (RVO). Just return x; — the compiler already moves or elides.

    📋 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

    Frequently Asked Questions

    Q: 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.

    Q: 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.

    Q: 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.

    Q: 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.

    Q: 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.

    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.

    🎯 Mini-Challenge: build the Document class

    Write the copy and move constructors yourself, then trigger each one.

    Try it Yourself »
    C++
    #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;        
    ...

    Pro Tips

    • 💡 Always mark move operations noexcept so std::vector and friends actually move during reallocation.
    • 💡 Prefer the Rule of Zero: if your members (vector, string, smart pointers) already manage their resources, write none of the five and let the compiler generate them.
    • 💡 Don't return move(local): plain return local; lets the compiler elide the move entirely.
    • 💡 Never read a moved-from value — treat it as empty until you reassign it.

    🎉 Lesson Complete

    • ✅ lvalues have a name and address; rvalues are temporaries you can safely steal from
    • T&& is an rvalue reference — it binds to temporaries
    • std::move is a cast that selects the move constructor / move assignment
    • ✅ Moving transfers ownership of a buffer in O(1) instead of copying it
    • ✅ The Rule of Five: define the destructor, both copies, and both moves together
    • ✅ Mark moves noexcept so std::vector moves (not copies) on reallocation
    • Next lesson: the Modern C++ Memory Model — how the compiler and CPU order memory

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service