Move in C++: std::move, Move Semantics, and Efficient Resource Transfers

move in C++ refers to move semantics and the std::move utility that let you transfer resources from one object to another instead of copying, enabling big performance wins for large or non-copyable objects. std::move doesn’t move by itself—it casts to an rvalue so a move constructor or move assignment can run.

This beginner-friendly guide explains move in C++ with clear code, outputs, pitfalls, best practices, and “Try it yourself” challenges after every section so you can master modern, efficient C++.

🔹 What is std::move and move semantics?

Move semantics let an object transfer ownership of its resources (like heap memory, file handles) to another object instead of duplicating them. std::move(expr) is a cast to an rvalue reference, telling the compiler “it’s safe to steal from this object now.” The actual move happens only if a move constructor or move assignment operator is available.

#include <utility> // std::move
#include <string>
#include <iostream>
using namespace std;
int main() {
    string a = "Hello, world!";
    string b = std::move(a); // moves contents of 'a' into 'b'
    cout << "b=" << b << "\n";
    cout << "a.size()=" << a.size() << "\n"; // 'a' is valid but unspecified
}

Output

b=Hello, world!
a.size()=0

Moved-from objects remain valid but in an unspecified state (often empty). Only use them for destruction, reassignment, or operations documented to work on empty states.

Try it yourself

  • Create a std::vector<int> with 1e6 elements, then auto v2 = std::move(v1). Print sizes before/after to see how fast moving is.
  • Call std::move on a const std::string and see why it still copies (moving from const usually falls back to copy).

🔹 Implementing move: move constructor and move assignment

To support move in C++, define a move constructor and move assignment that “steal” the resource and null the source. Mark them noexcept when possible so containers prefer moves during reallocation.

#include <algorithm>
#include <iostream>
using namespace std;
class Buffer {
    size_t n{0};
    int* p{nullptr};
public:
    Buffer() = default;
    explicit Buffer(size_t n_) : n(n_), p(new int[n_]{}) {}
    ~Buffer() { delete[] p; }
    // Copy semantics
    Buffer(const Buffer& other) : n(other.n), p(new int[n]) {
        std::copy(other.p, other.p + n, p);
        cout << "copy ctor\n";
    }
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            int* np = new int[other.n];
            std::copy(other.p, other.p + other.n, np);
            delete[] p;
            p = np;
            n = other.n;
            cout << "copy assign\n";
        }
        return *this;
    }
    // Move semantics
    Buffer(Buffer&& other) noexcept : n(other.n), p(other.p) {
        other.n = 0; other.p = nullptr;
        cout << "move ctor\n";
    }
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] p;
            n = other.n;
            p = other.p;
            other.n = 0; other.p = nullptr;
            cout << "move assign\n";
        }
        return *this;
    }
    size_t size() const { return n; }
};
int main() {
    Buffer a(1000000);
    Buffer b = std::move(a); // move ctor
    Buffer c;
    c = std::move(b);        // move assign
    cout << "a.size=" << a.size() 
         << ", b.size=" << b.size() 
         << ", c.size=" << c.size() << "\n";
}

Output

move ctor
move assign
a.size=0, b.size=0, c.size=1000000

Notice how moves avoid expensive allocations/copies. Containers can leverage this to reallocate efficiently when your move operations are noexcept.

Try it yourself

  • Remove noexcept and push many Buffer objects into std::vector. Inspect whether reallocation chooses copy over move in your toolchain.
  • Add a throwing move (for learning) and watch how containers behave; then restore noexcept.

🔹 move in C++ with move-only types (unique_ptr)

Some types are intentionally non-copyable but movable, like std::unique_ptr. You cannot copy them, but you can transfer ownership with std::move.

#include <memory>
#include <iostream>
using namespace std;
int main() {
    auto up = make_unique<int>(42);
    // auto up2 = up; // ERROR: unique_ptr is non-copyable
    auto up2 = std::move(up); // OK: up becomes empty
    cout << boolalpha << (up == nullptr) << " " << *up2 << "\n";
}

Output

true 42

Moving unique resources is often the primary reason to use move in C++. It makes ownership transfer explicit and safe.

Try it yourself

  • Store std::unique_ptr<int> in a std::vector. Use emplace_back(std::make_unique<int>(…)) and confirm it compiles and transfers ownership.
  • Write a function that returns std::unique_ptr<T>; move its return value into a caller-owned variable.

🔹 Using std::move correctly in containers (push_back vs emplace_back)

When you push objects into containers, you can avoid copies by moving. emplace_back constructs in place, often better than constructing then moving.

#include <vector>
#include <string>
#include <iostream>
using namespace std;
int main() {
    vector<string> v;
    string s = "abcdefghijklmnopqrstuvwxyz";
    v.push_back(s);             // copy
    v.push_back(std::move(s));  // move; s is now unspecified
    v.emplace_back(10, 'x');    // constructs "xxxxxxxxxx" directly
    cout << "sizes: " << v[0].size() << ", "
         << v[1].size() << ", "
         << v[2].size() << "\n";
}

Output

sizes: 26, 0, 10

Prefer emplace_back(args…) when you can construct directly. Prefer push_back(std::move(obj)) when you already have a fully constructed object you won’t use again.

Try it yourself

  • Reserve capacity with v.reserve(1000), then compare timings of push_back(s) vs push_back(std::move(s)) for large strings.
  • Experiment with a custom class that logs copy/move to visualize container behavior.

🔹 Return by value, RVO, and when to std::move

Since C++17, returning a local by value uses guaranteed copy elision (NRVO/RVO), so neither copy nor move occurs. Do not std::move a local variable at return; it can block elision and be slower.

#include <vector>
using namespace std;
vector<int> makeVec() {
    vector<int> v(1'000'000, 7);
    return v;        // C++17: guaranteed elision
    // return std::move(v); // DO NOT: may inhibit elision
}

Only std::move when returning non-local references or when overloading forces it. For locals, let the compiler elide the return.

Try it yourself

  • Create makeBig() returning a large std::string both with and without std::move. Inspect assembly or measure runtime in -O2; elision should win.
  • Return a member from a factory type by value; verify there’s no extra move under C++17.

🔹 std::move vs std::forward (quick comparison)

std::move unconditionally casts to an rvalue. std::forward conditionally casts based on a forwarding reference’s type parameter. Use forward in templates that pass arguments through; use move when you own the object and want to transfer it.

Featurestd::movestd::forward
PurposeCast to rvalue (enable moving)Preserve value category in templates
When to useTransferring owned objectForwarding function arguments
Effect on lvaluesMakes rvalueKeeps lvalue unless T is deduced as rvalue
#include <utility>
#include <string>
void sink(const std::string&); // lvalue overload
void sink(std::string&&);      // rvalue overload
template <typename T>
void relay(T&& x) {
    sink(std::forward<T>(x)); // forwards as lvalue/rvalue properly
}

Try it yourself

  • Add logging to each sink overload and call relay(s) and relay(std::string("tmp")) to see overload selection.
  • Replace forward with move and observe how you always call the rvalue overload (even for lvalues).

🔹 Common pitfalls with move in C++

  • std::move doesn’t move by itself: It only casts; moving happens if a move overload exists.
  • Avoid using moved-from objects: They’re valid but unspecified; reassign or destroy them.
  • Don’t move from const: Moving from const usually falls back to copying, because move overloads typically require non-const rvalues.
  • Don’t std::move at return of locals: It may inhibit copy elision.
  • Remember noexcept: Mark move operations noexcept so STL containers prefer them.
  • Be careful in operator=: Self-move is rare but possible; guard or use copy-and-swap.
// Example: self-move safe assignment via copy-and-swap
#include <utility>
class X {
public:
    void swap(X& other) noexcept {/* swap members */}
    X& operator=(X other) noexcept {
        // by value: copy or move into 'other'
        swap(other);
        return *this;
    }
};

Try it yourself

  • Create a type with logging copy/move and test assignments using copy-and-swap. Confirm strong exception safety.
  • Try to move from a const object and note that the copy constructor runs instead of the move constructor.

🔹 Mini project: Fast log buffer with move semantics

We will implement a small log entry type that is efficiently stored in a vector. Move semantics will avoid copies during growth and reordering.

#include <string>
#include <vector>
#include <iostream>
using namespace std;
struct LogEntry {
    string level, msg;
    LogEntry(string level, string msg) 
        : level(std::move(level)), msg(std::move(msg)) {}
    LogEntry(const LogEntry&) 
    { cout << "copy\n"; }
    LogEntry(LogEntry&&) noexcept 
    { cout << "move\n"; }
    LogEntry& operator=(const LogEntry&) 
    { cout << "copy assign\n"; return *this; }
    LogEntry& operator=(LogEntry&&) noexcept 
    { cout << "move assign\n"; return *this; }
};
int main() {
    vector<LogEntry> logs;
    logs.reserve(2); // reduce reallocations to highlight moves
    logs.emplace_back("INFO", "Starting");
    logs.emplace_back("WARN", "Low memory");
    logs.emplace_back("ERROR", string(1000, 'X')); 
    // may reallocate and move existing entries
    for (const auto& e : logs) {
        cout << e.level << ": " 
        << e.msg.substr(0, 10) << "...\n";
    }
}

Output

move
INFO: Starting...
WARN: Low memor...
ERROR: XXXXXXXXXX...

Notice moves instead of copies when the vector grows. Construction also uses std::move to initialize members efficiently from temporaries.

Try it yourself

  • Remove noexcept from the move constructor and see if copies happen during reallocation (implementation-dependent).
  • Reserve different capacities and watch copy vs move counts.

🔹 Best practices for move in C++

  • Use std::move when you are done with an object and want to transfer its resources.
  • Mark move constructor and move assignment noexcept whenever you can.
  • Don’t std::move local variables on return; let copy elision do its job.
  • Prefer emplace_back and perfect forwarding in factory helpers for efficient construction.
  • Do not move from objects you still need to use meaningfully.
  • Expect moved-from objects to be valid but empty; design your types to support that invariant.

🔹 FAQs about move in C++

Q1: Does std::move actually move the object?
No. It casts to an rvalue. The move happens only if a move constructor/assignment exists for that type.

Q2: Is a moved-from object usable?
Yes, it must be valid but in an unspecified (often empty) state. You can assign to it, destroy it, or call operations that are documented to work on empty objects.

Q3: Why does moving from const often copy?
Move constructors typically take T&& (non-const rvalue). If the object is const, moving would require modifying it (to empty it), which isn’t allowed, so the copy constructor is chosen.

Q4: Should I mark move operations noexcept?
Yes, if possible. Containers prefer moving only if move is noexcept, otherwise they may fall back to copying.

Q5: When should I use std::move vs std::forward?
Use std::move when transferring ownership of a known object. Use std::forward inside template forwarding functions to preserve the input’s value category.

Q6: Do I ever need std::move on return?
Normally no for locals (C++17 guarantees elision). You might use it when returning a function parameter or a data member to select the rvalue overload.

Q7: Can I safely self-move in assignment?
It’s rare but possible. Implement move assignment to tolerate self-move (or use copy-and-swap idiom).

Q8: Does move always help performance?
Often, but measure. For small trivially copyable types (e.g., int), copies are as cheap as moves.

Q9: Is std::move safe across threads?
Yes, but like any object access, you must ensure no data races. Moving doesn’t add synchronization.

Q10: Does move change the object’s address?
Moving the object itself doesn’t relocate it, but its resources may change (e.g., pointers inside). A moved-from container typically releases its buffer to the destination.

🔹 Wrapping up

move in C++ lets you transfer resources instead of copying, unlocking major performance improvements and enabling ownership-safe designs with types like std::unique_ptr. Use std::move when you’re done with an object, implement noexcept move operations, prefer emplace where possible, and let C++17 return value optimization do the heavy lifting on returns. Practice the “Try it yourself” tasks above to build intuition and write efficient modern C++.

Leave a Comment

About RadiantRiva

Your go-to resource for coding tutorials, developer guides, and programming tips.

Learn More

Quick Links

Follow Us

Newsletter

Get coding tips, tutorials, and updates straight to your inbox.