Memory Management

Move Semantics

Master C++11 move semantics — lvalue vs rvalue, move constructor, move assignment, std::move, perfect forwarding, and eliminating unnecessary copies.

Move Semantics

Move semantics (C++11) allow a program to transfer ownership of resources rather than copy them. Before C++11, returning a vector from a function copied all its elements. With move semantics, the function transfers the internal buffer — an O(1) operation instead of O(n). Modern C++ relies on move semantics for performance throughout the standard library.


Lvalues and Rvalues

cpp
int x = 42;   // x is an lvalue (has a name, has an address)
int y = x;    // Copies x into y

// Rvalues are temporaries — no name, no persistent address
int z = x + 1;    // (x + 1) is an rvalue temporary
                   // After this line, the temporary is gone

// Lvalue reference — binds to named objects
int& lref = x;    // OK
// int& lref2 = 42;  // ERROR — 42 is an rvalue

// Rvalue reference (C++11) — binds to temporaries
int&& rref = 42;      // OK
int&& rref2 = x + 1;  // OK — binds to temporary
// int&& rref3 = x;   // ERROR — x is an lvalue

The Cost of Copying

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

class BigData {
    vector<int> data;
public:
    BigData(size_t n) : data(n, 0) {
        cout << "Constructed (" << n << " elements)\n";
    }

    // Copy constructor — copies all n elements
    BigData(const BigData& other) : data(other.data) {
        cout << "Copied (" << data.size() << " elements)\n";
    }

    // Move constructor — just swaps the internal pointer
    BigData(BigData&& other) noexcept : data(move(other.data)) {
        cout << "Moved (O(1) — just transferred pointer)\n";
    }

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

int main() {
    BigData original(1000000);     // Constructed (1,000,000 elements)

    BigData copy = original;       // Copied (1,000,000 elements) — expensive!
    BigData moved = move(original); // Moved — O(1), just a pointer swap

    cout << "copy.size()  = " << copy.size()  << "\n";   // 1000000
    cout << "moved.size() = " << moved.size() << "\n";   // 1000000
    cout << "original.size() = " << original.size() << "\n"; // 0 (moved-from)

    return 0;
}

Move Constructor and Move Assignment

cpp
#include <iostream>
#include <utility>
using namespace std;

class Buffer {
    int*   data;
    size_t size_;

public:
    Buffer(size_t n) : data(new int[n]()), size_(n) {
        cout << "Constructed " << n << " ints\n";
    }

    // Copy constructor — deep copy
    Buffer(const Buffer& other) : data(new int[other.size_]), size_(other.size_) {
        copy(other.data, other.data + size_, data);
        cout << "Copied " << size_ << " ints\n";
    }

    // Move constructor — steal the pointer
    Buffer(Buffer&& other) noexcept
        : data(other.data), size_(other.size_) {
        other.data  = nullptr;   // Leave source in valid empty state
        other.size_ = 0;
        cout << "Moved (O(1))\n";
    }

    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data;          // Free existing resource
            data        = other.data;
            size_       = other.size_;
            other.data  = nullptr;
            other.size_ = 0;
            cout << "Move-assigned\n";
        }
        return *this;
    }

    ~Buffer() {
        delete[] data;
        cout << "Destroyed " << size_ << " ints\n";
    }

    size_t size() const { return size_; }
};

int main() {
    Buffer b1(100);            // Constructed 100 ints
    Buffer b2 = move(b1);     // Moved (O(1)) — b1 is now empty
    cout << b1.size() << "\n"; // 0
    cout << b2.size() << "\n"; // 100

    Buffer b3(50);
    b3 = move(b2);            // Move-assigned
    cout << b3.size() << "\n"; // 100

    return 0;
}

std::move

std::move is just a cast — it casts an lvalue to an rvalue reference, enabling move semantics:

cpp
#include <iostream>
#include <string>
#include <utility>
using namespace std;

int main() {
    string s1 = "Hello, World!";

    // Without move — copies the string
    string s2 = s1;
    cout << s1 << "\n";   // Hello, World! (unchanged)
    cout << s2 << "\n";   // Hello, World!

    // With std::move — transfers the buffer
    string s3 = move(s1);
    cout << s1 << "\n";   // "" (moved-from — valid but unspecified)
    cout << s3 << "\n";   // Hello, World!

    // Inserting into vector with move
    vector<string> v;
    string big = "Large string that should not be copied";
    v.push_back(move(big));   // Moves — no copy
    cout << big << "\n";      // "" (moved-from)

    return 0;
}

Warning: After std::move(x), the moved-from object x is in a valid but unspecified state. You can assign to it again or destroy it — but don't read its value.


Return Value Optimisation (RVO)

When you return a local object by value, the compiler often eliminates the copy entirely:

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

vector<int> makeVector(int n) {
    vector<int> result;
    for (int i = 0; i < n; i++) result.push_back(i);
    return result;    // RVO — the compiler constructs result directly in the caller
}

int main() {
    auto v = makeVector(5);   // No copy, no move — constructed in place
    for (int x : v) cout << x << " ";
    cout << "\n";   // 0 1 2 3 4
    return 0;
}

Named Return Value Optimisation (NRVO) applies when you return a named local variable. Modern compilers perform this optimization automatically — don't force return std::move(result) as it can disable RVO.


Perfect Forwarding

Perfect forwarding preserves the value category (lvalue/rvalue) of arguments when passing them to another function:

cpp
#include <iostream>
#include <utility>
using namespace std;

void process(int& x)  { cout << "lvalue: " << x << "\n"; }
void process(int&& x) { cout << "rvalue: " << x << "\n"; }

// Template with forwarding reference (T&&)
template <typename T>
void forwardToProcess(T&& arg) {
    process(forward<T>(arg));   // Preserves lvalue/rvalue nature
}

int main() {
    int x = 42;

    forwardToProcess(x);    // lvalue: 42  (lvalue — x has a name)
    forwardToProcess(99);   // rvalue: 99  (rvalue — temporary)
    forwardToProcess(x+1);  // rvalue: 43  (rvalue — temporary)

    return 0;
}

std::forward is used in variadic templates and factory functions like make_unique:

cpp
template <typename T, typename... Args>
unique_ptr<T> myMakeUnique(Args&&... args) {
    return unique_ptr<T>(new T(forward<Args>(args)...));
}

noexcept and Move Semantics

Mark move constructors and move assignments as noexcept — this allows the standard library to use move operations in critical paths:

cpp
class Safe {
    int* data;
public:
    // noexcept — tells std::vector it's safe to move during reallocation
    Safe(Safe&& other) noexcept : data(other.data) {
        other.data = nullptr;
    }

    Safe& operator=(Safe&& other) noexcept {
        swap(data, other.data);
        return *this;
    }
};

Without noexcept, std::vector falls back to copying during reallocation for exception safety.


Practical Example: Move-Enabled String Builder

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

class StringBuilder {
    vector<string> parts;

public:
    // Append — copy
    StringBuilder& append(const string& s) {
        parts.push_back(s);
        return *this;
    }

    // Append — move (avoids copy for temporaries)
    StringBuilder& append(string&& s) {
        parts.push_back(move(s));
        return *this;
    }

    // Build — returns assembled string (RVO applies)
    string build() const {
        string result;
        for (const auto& part : parts) result += part;
        return result;
    }

    // Move the builder itself
    StringBuilder(StringBuilder&&) noexcept = default;
    StringBuilder& operator=(StringBuilder&&) noexcept = default;

    StringBuilder() = default;
};

StringBuilder makeQuery(const string& table, const vector<string>& columns) {
    StringBuilder sb;
    sb.append("SELECT ");

    for (size_t i = 0; i < columns.size(); i++) {
        sb.append(columns[i]);
        if (i + 1 < columns.size()) sb.append(", ");
    }

    sb.append(" FROM ").append(table).append(";");
    return sb;   // NRVO — no copy
}

int main() {
    vector<string> cols = {"id", "name", "email"};
    StringBuilder  qb   = makeQuery("users", cols);

    cout << qb.build() << "\n";
    // SELECT id, name, email FROM users;

    // Move the builder
    StringBuilder qb2 = move(qb);
    cout << qb2.build() << "\n";  // Still works
    // SELECT id, name, email FROM users;

    return 0;
}

Summary

  • An lvalue has a name and address; an rvalue is a temporary without persistent storage
  • The move constructor and move assignment steal resources from temporaries rather than copying them
  • std::move(x) casts x to an rvalue reference — it does not actually move anything by itself
  • After moving from an object, it is in a valid but unspecified state — don't read it, but you can reassign or destroy it
  • RVO/NRVO eliminates copy/move entirely when returning local objects — never write return std::move(local)
  • std::forward in template code preserves lvalue/rvalue nature — the basis of perfect forwarding
  • Always mark move operations noexcept — std::vector and other containers rely on this for optimised reallocations

Next, we'll explore the C++ Standard Template Library — starting with vectors and arrays, the most commonly used containers.