CODE: CPP Move Semantics

Before C++11, C++ had RAII, but it lacked efficient ownership transfer.Objects were either copied or destroyed. There was no middle ground.

That limitation made certain designs inefficient:

  • Returning large objects was expensive.
  • Containers had to copy elements during reallocation.
  • Smart pointer ownership transfer was awkward.
  • Performance-critical systems required hacks.

Move semantics changed the language’s core model of value transfer.

It introduced the idea that:

Resources don’t have to be duplicated. They can be transferred.

This single idea reshaped modern C++.

Move semantics exists because C++ distinguishes between different kinds of expressions.

An object can be:

  • lvalue → has identity and stable address
  • rvalue → temporary, about to die

If an object is temporary, copying it is wasteful.

It will be destroyed anyway.

Example

cpp
std::string s = "hello";
std::string t = s;          // copy (s is lvalue)
std::string u = "world";    // move (temporary)

Intuition:

md
lvalue:
+--------+
|   s    |  stable memory
+--------+

rvalue:
temporary created → used → destroyed

Move semantics activates when the compiler sees a temporary.


Lvalue References (`T&`) — The Original Reference

Before C++11, there was only one kind of reference:

cpp
T&

This is what people informally call:

“left-side reference”

Because it binds to something that can appear on the left side of assignment.

Example:

cpp
int x = 5;
int& ref = x;

Here:

  • x is an lvalue
  • ref is an lvalue reference
  • ref becomes an alias of x

What Is an Lvalue?

An lvalue is something that:

  • Has identity
  • Has a stable memory address
  • Can appear on the left side of assignment

Example:

cpp
int a = 10;
a = 20;     // valid → lvalue

md
Memory:
+-----+
|  a  |  ← stable address
+-----+

ref →
       |
       v
+-----+
|  a  |
+-----+


Rvalue References — The Enabler

C++11 introduced:

cpp
T&&

This binds specifically to rvalues.

Example:

cpp
int&& x = 10;

10 is temporary → x binds to it.

But the true power appears inside classes.


The Move Constructor — Stealing Resources

When a temporary object is used to initialize another object, the move constructor is called.

Let’s build a manual resource-owning type.

cpp
class Buffer {
    int* data;
    size_t size;

public:
    Buffer(size_t s) : size(s) {
        data = new int[size];
    }

    // Move constructor
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size)
    {
        other.data = nullptr;
        other.size = 0;
    }

    ~Buffer() {
        delete[] data;
    }
};

Instead of copying memory, we transfer pointer ownership.

Ownership Transfer Diagram

Before move:

md
other
+------------------+
| data -> [memory] |
| size             |
+------------------+

After move:

md
this
+------------------+
| data -> [memory] |
| size             |
+------------------+

other
+------------------+
| data = nullptr   |
| size = 0         |
+------------------+

No allocation.

No duplication.

Just pointer reassignment.

That is zero-overhead abstraction.


Move Assignment Operator — Rebinding Ownership

Initialization and assignment are different operations.

Assignment must:

  • Release current resource
  • Take ownership of new one

cpp
Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {
        delete[] data;

        data = other.data;
        size = other.size;

        other.data = nullptr;
        other.size = 0;
    }
    return *this;
}

The object remains valid, but its internal state changes.


Why We Must Reset the Source

If we do not nullify the moved-from object:

Two objects would delete the same memory.

This leads to double free → undefined behavior.

Move semantics requires that:

  • The moved-from object remains valid.
  • But its state is unspecified.

This is a critical guarantee in modern C++.


Rule of Five — Resource Management Discipline

If your class manages resources manually, you must think about:

  • Destructor
  • Copy constructor
  • Copy assignment
  • Move constructor
  • Move assignment

This is called the Rule of Five.

Why five?

Because move semantics introduced two additional special member functions.

If you define one, you probably need to define all relevant ones.

Modern C++ philosophy:

If you manage raw memory manually, you accept responsibility for all ownership semantics.

std::move — What It Actually Does

This is one of the most misunderstood functions.

cpp
std::move(x)

It does not move anything.

It performs:

cpp
static_cast<T&&>(x)

It converts an lvalue into an rvalue reference.

It tells the compiler:

“You may treat this object as temporary.”

Example:

cpp
std::string a = "hello";
std::string b = std::move(a);

After this:

  • b owns the memory
  • a is valid but unspecified

Copy vs Move — Performance View

Copy:

md
A ---> [memory]
B ---> [new memory copy]

Move:

md
A ---> nullptr
B ---> [memory]

Copy complexity: O(n)

Move complexity: O(1)

This difference matters in:

  • Large containers
  • Real-time systems
  • Embedded Linux pipelines
  • High-frequency systems

For senior roles, this distinction must be intuitive.


What Is `noexcept` in C++

noexcept is a specifier that tells the compiler:

“This function is guaranteed NOT to throw exceptions.”

Basic example:

cpp
void f() noexcept {
    // this function promises not to throw
}

If an exception escapes this function →

the program calls:

cpp
std::terminate();

It does not unwind the stack normally.


Why `noexcept` Exists

Before C++11, we had:

md
void f() throw();

But dynamic exception specifications were slow and complex.

C++11 replaced them with: noexcept

It is:

  • Simpler
  • Faster
  • Compile-time friendly
  • Required for move optimization

What Happens If a `noexcept` Function Throws?

Example:

cpp
void f() noexcept {
    throw std::runtime_error("error");
}

At runtime:

md
std::terminate() is called

No stack unwinding.

Immediate termination.

So noexcept is a contract.


Why `noexcept` Is Critical for Move Semantics

This is the most important part.

Consider:

cpp
std::vector<MyType> v;

When vector reallocates (grows capacity):

It must move or copy elements.

But what if moving throws?

Vector must preserve strong exception guarantee:

If reallocation fails, original data must remain intact.

So the rule is:

md
If move constructor is noexcept → use move
If not noexcept → use copy

Example:

cpp
class A {
public:
    A(A&&) noexcept { }
};

Vector will move A during reallocation.

If you remove noexcept:

cpp
A(A&&) { }

Vector may fallback to copying.

This silently destroys performance.