Skillversitas

Move Semantics & Rvalue References in C++

Move semantics is a C++11 feature that enables transferring ownership of a resource from one object to another without copying the data. This is very important for performance because it avoids expensive memory allocation and data copying, especially for large objects such as strings, vectors, or objects that manage resources.

To understand move semantics, we need to understand the difference between lvalues and rvalues:

  • Lvalue — An object that has a name and an identity (a fixed memory address). It can be on the left side of an assignment. Example: int x = 10; → x is an lvalue.
  • Rvalue — A temporary object that has no name and usually exists only briefly. It can be on the right side of an assignment. Examples: 10, x + 1, string("Hello") → all rvalues.

The && operator (rvalue reference) is the key to move semantics. It allows us to detect when an object is a temporary (an rvalue) and "move" its resource instead of copying it. std::move explicitly converts an lvalue into an rvalue reference, telling the compiler that we want to move that object.

std::forward is used in perfect forwarding to preserve the value category (lvalue/rvalue) when forwarding arguments to another function. This is important in templates to ensure correct behavior.

emplace_back vs push_back: emplace_back uses forwarding to construct the object in place directly, avoiding the creation of a temporary object and any move/copy.

Code Example

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

// ==================== Basic Lvalue and Rvalue ====================
void basicLvalueRvalue() {
    cout << "=== Basic Lvalue and Rvalue ===" << endl;

    int x = 10;         // x is an lvalue, 10 is an rvalue
    int y = x + 5;      // y is an lvalue, (x + 5) is an rvalue
    string s1 = "Hi";   // s1 is an lvalue, "Hi" is a temporary rvalue

    // Lvalue reference
    int& ref = x;       // ref refers to x (an lvalue)
    ref = 20;
    cout << "x = " << x << endl;  // x = 20

    // Rvalue reference
    int&& rref = 100;   // rref refers to the rvalue 100
    cout << "rref = " << rref << endl;

    // std::move converts an lvalue into an rvalue reference
    int z = 42;
    int&& moved = move(z);
    cout << "z = " << z << ", moved = " << moved << endl;
    // z still holds 42, but it can now be "moved"
}

// ==================== Move Constructor ====================
class MyString {
private:
    char* data;
    int length;

public:
    // Regular constructor (deep copy)
    MyString(const char* str = "") {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
        cout << "Constructor: \"" << data << "\" (length=" << length << ")" << endl;
    }

    // Copy constructor (deep copy - expensive!)
    MyString(const MyString& other) {
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
        cout << "Copy constructor: \"" << data << "\"" << endl;
    }

    // Move constructor (transfer ownership - cheap!)
    MyString(MyString&& other) noexcept {
        data = other.data;      // Take the pointer
        length = other.length;  // Take the length

        // Set the source to a safe state
        other.data = nullptr;
        other.length = 0;
        cout << "Move constructor: \"" << data << "\"" << endl;
    }

    // Copy assignment operator
    MyString& operator=(const MyString& other) {
        if (this != &other) {
            delete[] data;
            length = other.length;
            data = new char[length + 1];
            strcpy(data, other.data);
            cout << "Copy assignment: \"" << data << "\"" << endl;
        }
        return *this;
    }

    // Move assignment operator
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] data;  // Free the old resource

            data = other.data;      // Transfer ownership
            length = other.length;

            other.data = nullptr;
            other.length = 0;
            cout << "Move assignment: \"" << data << "\"" << endl;
        }
        return *this;
    }

    ~MyString() {
        cout << "Destructor: \"" << (data ? data : "nullptr") << "\"" << endl;
        delete[] data;
    }

    void display() const {
        cout << "String: \"" << (data ? data : "nullptr") << "\" (length=" << length << ")" << endl;
    }
};

// ==================== Perfect Forwarding ====================
template <typename T>
void wrapper(T&& arg) {
    // std::forward preserves the value category
    cout << "wrapper: forwarding " << arg << endl;
}

class Processor {
public:
    void process(string& s) {
        cout << "Process lvalue: " << s << endl;
    }

    void process(string&& s) {
        cout << "Process rvalue: " << s << endl;
    }
};

template <typename T>
void perfectForward(Processor& proc, T&& value) {
    // std::forward preserves whether value is an lvalue or an rvalue
    proc.process(forward<T>(value));
}

// ==================== emplace_back vs push_back ====================
void emplaceVsPush() {
    cout << "\n=== emplace_back vs push_back ===" << endl;

    vector<MyString> vec;

    cout << "--- push_back ---" << endl;
    MyString temp("Hello");
    vec.push_back(temp);           // Copy or move temp

    cout << "\n--- emplace_back ---" << endl;
    vec.emplace_back("World");     // Construct in place directly
    // No temporary object needed, no copy/move needed

    cout << "\n--- push_back with an rvalue ---" << endl;
    vec.push_back(MyString("Move me"));  // Move (temporary object)

    cout << "\n--- emplace_back directly ---" << endl;
    vec.emplace_back("Direct");    // Direct construction
}

// ==================== Move Optimization ====================
vector<int> createLargeVector(int n) {
    vector<int> v(n);
    for (int i = 0; i < n; i++) {
        v[i] = i;
    }
    return v;  // NRVO (Named Return Value Optimization) or a move
}

int main() {
    basicLvalueRvalue();

    // ==================== Move Semantics Demo ====================
    cout << "\n=== Move Semantics Demo ===" << endl;

    MyString s1("First");
    cout << "\n--- Copy ---" << endl;
    MyString s2 = s1;  // Copy constructor
    s1.display();
    s2.display();

    cout << "\n--- Move ---" << endl;
    MyString s3 = move(s1);  // Move constructor
    s1.display();  // nullptr after the move
    s3.display();

    cout << "\n--- Move Assignment ---" << endl;
    MyString s4("Fourth");
    s4 = move(s3);  // Move assignment
    s3.display();  // nullptr after the move
    s4.display();

    // ==================== Perfect Forwarding ====================
    cout << "\n=== Perfect Forwarding ===" << endl;

    Processor proc;
    string name = "John";

    perfectForward(proc, name);        // Forward as an lvalue
    perfectForward(proc, string("Peter"));  // Forward as an rvalue

    // ==================== emplace_back vs push_back ====================
    emplaceVsPush();

    // ==================== Performance Example ====================
    cout << "\n=== Performance Example ===" << endl;
    {
        vector<MyString> performance;
        cout << "push_back temporary:" << endl;
        performance.push_back(MyString("Test"));

        cout << "\nemplace_back directly:" << endl;
        performance.emplace_back("Test");
    }

    // ==================== Move with a vector ====================
    cout << "\n=== Move with Vector ===" << endl;
    {
        vector<int> v1 = createLargeVector(5);
        cout << "v1 size: " << v1.size() << endl;

        // Move v1 into v2
        vector<int> v2 = move(v1);
        cout << "v2 size: " << v2.size() << endl;
        cout << "v1 size after move: " << v1.size() << endl;
    }

    cout << "\n=== Best Practices ===" << endl;
    cout << "1. Use std::move() to move lvalues that are no longer used." << endl;
    cout << "2. Write a move constructor and a move assignment operator for classes that manage resources." << endl;
    cout << "3. Mark move operations as noexcept for container optimization." << endl;
    cout << "4. Use emplace_back() instead of push_back() to avoid temporary objects." << endl;
    cout << "5. Use perfect forwarding for template functions that forward arguments." << endl;

    return 0;
}

Code Explanation

  1. int&& rref = 100 — An rvalue reference referring to the temporary value 100. This is the foundation of move semantics.

  2. std::move(z) — Converts z (an lvalue) into an rvalue reference. It does not actually "move" anything; it only changes the value's category.

  3. MyString(MyString&& other) noexcept — A move constructor. Takes the resource from other without copying. After the move, other is in a safe state (nullptr).

  4. data = other.data — In the move constructor, we only copy the pointer (very cheap) instead of copying the entire character array (very expensive).

  5. noexcept — Indicates that the move constructor/assignment does not throw an exception. Important for std::vector, which uses moves when reallocating.

  6. operator=(MyString&& other) — A move assignment operator. Same as the move constructor but for assignment. It must handle self-assignment and clean up the old resource.

  7. template <typename T> void wrapper(T&& arg) — A universal reference (not a regular rvalue reference). T&& in a template can refer to either an lvalue or an rvalue depending on the argument.

  8. std::forward<T>(value) — Perfect forwarding. Preserves the value category (lvalue/rvalue) when forwarding an argument to another function.

  9. vec.emplace_back("World") — Constructs the object directly inside the vector. More efficient than push_back(MyString("World")) because it avoids a temporary object.

Everyday Use Cases

Imagine you are building an image processing application. Each image is large (hundreds of MB). Without move semantics, copying an image into a function or container would be very slow because it would have to reallocate memory and copy all the data.

class Image {
    unsigned char* pixels;
    int width, height;
public:
    // Move constructor - just move the pointer, don't copy the pixels!
    Image(Image&& other) noexcept
        : pixels(other.pixels), width(other.width), height(other.height) {
        other.pixels = nullptr;
    }
};

vector<Image> gallery;
gallery.push_back(Image("photo.jpg"));  // Move, not copy!

// Even better with emplace_back
gallery.emplace_back("photo.jpg");  // Direct construction in place

Conclusion

Move semantics enables efficient resource transfer without copying data. Rvalue references (&&) detect temporary objects that can be moved. std::move converts an lvalue into an rvalue, and std::forward preserves the value category for perfect forwarding. emplace_back is more efficient than push_back because it constructs the object directly in place. Always write a move constructor/assignment for classes that manage dynamic resources.