C++ Move Semantics Tutorial: Trace Ownership with Runnable Examples

Follow one heap allocation through copy and move operations, then learn how value categories, special members, copy elision, and noexcept affect real C++ code.

KnowledgeGate Team

Exam prep & CS education

Updated 25 Aug 20265 min read

std::move(a) neither moves bytes nor guarantees that a becomes empty. It enables an operation that may transfer ownership. Tracing 7, 11, 13, 17 through a runnable Buffer shows why noexcept affects containers. The Coding and Skill Development courses connect this ownership model to broader programming practice.

The ownership problem that move semantics solves

Buffer a{7, 11, 13, 17} owns four heap integers. Copying needs another allocation and four copied values. Moving can transfer the pointer and size, leaving a safe to destroy or assign.

Every live block must have one owner and one deletion. A moved-from object must remain valid, but its value may be unspecified. Our class deliberately makes it empty: size_ == 0 and data_ == nullptr.

A deep copy handles n values; this move transfers a pointer and size.

Copy makes b a separate block, while std::move transfers a's block to c and leaves a empty.

lvalues, rvalues and what std::move does

Named objects such as word are lvalues; temporaries such as std::string{"move"} are rvalues. T& binds to a modifiable lvalue, const T& to either category, and T&& commonly marks a consuming overload.

#include <iostream>
#include <string>
#include <utility>

void tag(const std::string& text) {
    std::cout << "lvalue overload: " << text << '\n';
}

void tag(std::string&& text) {
    std::cout << "rvalue overload: " << text << '\n';
}

int main() {
    std::string word = "move";
    tag(word);
    tag(std::move(word));
    std::cout << "after tag: " << word << '\n';
}

The exact output is:

lvalue overload: move
rvalue overload: move
after tag: move

std::move is a cast that permits rvalue-overload selection. This overload only reads, so word stays unchanged. A named rvalue reference is an lvalue expression: tag(alias) selects const std::string&, while tag(std::move(alias)) selects std::string&&. Functions in C reviews parameter passing; C has no move constructors or rvalue references.

Worked example: a Buffer that copies or transfers

Save this as MoveSemanticsDemo.cpp:

#include <algorithm>
#include <cstddef>
#include <initializer_list>
#include <iostream>
#include <utility>

class Buffer {
    std::size_t size_{0};
    int* data_{nullptr};

public:
    Buffer(std::initializer_list<int> values)
        : size_(values.size()),
          data_(size_ == 0 ? nullptr : new int[size_]) {
        if (size_ != 0) {
            std::copy(values.begin(), values.end(), data_);
        }
    }

    ~Buffer() {
        delete[] data_;
    }

    Buffer(const Buffer& other)
        : size_(other.size_),
          data_(size_ == 0 ? nullptr : new int[size_]) {
        if (size_ != 0) {
            std::copy(other.data_, other.data_ + size_, data_);
        }
        std::cout << "copy constructor\n";
    }

    Buffer(Buffer&& other) noexcept
        : size_(std::exchange(other.size_, 0)),
          data_(std::exchange(other.data_, nullptr)) {
        std::cout << "move constructor\n";
    }

    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            Buffer temporary(other);
            swap(temporary);
        }
        return *this;
    }

    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            size_ = std::exchange(other.size_, 0);
            data_ = std::exchange(other.data_, nullptr);
        }
        return *this;
    }

    void swap(Buffer& other) noexcept {
        std::swap(size_, other.size_);
        std::swap(data_, other.data_);
    }

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

    int& operator[](std::size_t index) {
        return data_[index];
    }

    void print(const char* name) const {
        std::cout << name << "=[";
        for (std::size_t i = 0; i < size_; ++i) {
            if (i != 0) {
                std::cout << ", ";
            }
            std::cout << data_[i];
        }
        std::cout << "]\n";
    }
};

int main() {
    Buffer a{7, 11, 13, 17};
    Buffer b = a;
    b[0] = 70;
    Buffer c = std::move(a);

    std::cout << "a.size=" << a.size() << '\n';
    b.print("b");
    c.print("c");
}

Run g++ -std=c++17 -Wall -Wextra -pedantic MoveSemanticsDemo.cpp && ./a.out. The output is:

copy constructor
move constructor
a.size=0
b=[70, 11, 13, 17]
c=[7, 11, 13, 17]

The copy gives b independent storage, so b[0] = 70 cannot alter Block A. The move uses std::exchange to take both members and install {0, nullptr} in a; delete[] nullptr is safe.

Four frames trace a, b, and c as a copy duplicates a block, an edit stays local, and std::move hands ownership to c.

Pointers in C for GATE reviews heap-address diagrams. A pointer is only an address; the Buffer contract assigns ownership and deletion.

Rule of Zero, Rule of Five and move-only ownership

Because Buffer owns raw storage, it defines all five special operations. A shallow copy would make two objects delete one array. Its copy assignment replaces through a temporary; move assignment guards self-move, releases, then steals.

Prefer the Rule of Zero in application code:

struct Record { std::string name; std::vector<int> marks; };
Record r{"Asha", {72, 81, 94}};

These members manage themselves. Special-member declarations can suppress implicit moves; std::move cannot create one.

For sole ownership, auto p = std::make_unique<int>(42); auto q = std::move(p); leaves p == nullptr and *q == 42; copying is disabled.

Return values, copy elision and noexcept

Under C++17, Buffer make_primes() { return Buffer{2, 3, 5}; } can initialise its destination directly. For a named local, return result; may use named return value optimisation, with move as fallback. return std::move(result); can block that optimisation.

Suppose a vector<Packet> reserves one slot, inserts 10, then 20. With a noexcept move, growth relocates 10 once, printing move 10. If move may throw and copy exists, it may print copy 10 to preserve its exception guarantee. A false noexcept can cause termination. Buffer only exchanges a size and pointer, so its promise is truthful.

Common move-semantics mistakes and repairs

Mistake

What goes wrong

Repair

Assuming std::move always moves

A cast transfers nothing

Check the overload and whether it consumes

Reading an undocumented moved-from value

It may be unspecified

Destroy, assign, or use documented operations

Not resetting a source pointer

Two destructors may delete one block

Exchange with nullptr; reset size

Moving from const Buffer and expecting move construction

Buffer&& cannot bind to const

Expect an available const copy overload

Writing return std::move(local)

It can block named-return optimisation

Write return local

Omitting a destructive self-move guard

An object may release its resource

Check this != &other

Marking a throwing move noexcept

An exception can terminate

Use it only when true

For const Buffer fixed{5, 8}; Buffer next = std::move(fixed);, this class copies because Buffer&& cannot bind to const. Both objects contain [5, 8]. After our earlier move, a.size() is 0, so a[0] is invalid.

How assessments test moves, with checkable exercises

Questions ask you to select overloads, trace constructors, find double deletion, or explain a container copy.

Classify the expression, select an overload, decide whether it consumes, record both states, then count owners. Here c owns [7, 11, 13, 17], a.size() is 0, and Block A is deleted once.

  1. Add Buffer d = b; d[1] = 110;. Answer: b stays [70, 11, 13, 17]; d is [70, 110, 13, 17].

  2. Add Buffer e = std::move(c);. Answer: c.size() is 0; e holds [7, 11, 13, 17].

  3. Predict const Buffer fixed{5, 8}; Buffer next = std::move(fixed);. Answer: copy constructor.

  4. Omit the source reset in a hand-written move. Answer: both objects retain one pointer and may delete it. Reset pointer and size.

Practise constructor traces and ownership questions in the Coding for Placements course.

The short version and the next step

  • A move transfers only when the selected operation implements transfer.

  • std::move is an rvalue cast.

  • Moved-from objects remain valid, but their value may be unspecified.

  • Resource owners must prevent shared deletion.

  • Truthful noexcept can let containers relocate by move.

  • Prefer Rule-of-Zero types when library members can own resources.

b owns [70, 11, 13, 17]; c owns the original [7, 11, 13, 17]; a is empty by this class's contract. Continue with the C++ Programming course for structured practice.