Menu
Coddy logo textTech

Move Semantics & Rvalues

Part of the Object Oriented Programming section of Coddy's C++ journey — lesson 83 of 104.

In C++, every expression is either an lvalue (has a persistent identity, can be addressed) or an rvalue (temporary, about to be destroyed). Understanding this distinction unlocks move semantics - a powerful optimization that avoids unnecessary copying.

An rvalue reference, declared with &&, binds specifically to temporary objects. This lets you "steal" resources from objects that are about to disappear anyway:

#include <iostream>
#include <utility>

class Buffer {
    int* data;
    size_t size;
public:
    Buffer(size_t s) : data(new int[s]), size(s) {
        std::cout << "Constructed\n";
    }
    
    // Move constructor - steals resources
    Buffer(Buffer&& other) noexcept 
        : data(other.data), size(other.size) {
        other.data = nullptr;  // Leave source in valid state
        other.size = 0;
        std::cout << "Moved\n";
    }
    
    ~Buffer() { delete[] data; }
};

int main() {
    Buffer b1(1000);
    Buffer b2(std::move(b1));  // Triggers move constructor
}

The std::move function doesn't actually move anything - it simply casts an lvalue to an rvalue reference, signaling that you're willing to give up the object's resources. The actual moving happens in the move constructor or move assignment operator.

Move semantics dramatically improve performance when working with resource-heavy objects like containers or strings. Instead of deep copying megabytes of data, you simply transfer pointer ownership - a constant-time operation regardless of size.

challenge icon

Challenge

Easy

Let's build a resource-managing DataBuffer class that demonstrates move semantics in action. You'll see how moving resources instead of copying them can dramatically improve efficiency when transferring ownership of dynamically allocated memory.

You'll organize your code across three files:

  • DataBuffer.h: Define your DataBuffer class that manages a dynamically allocated array of integers.

    Your class should have private members for the data pointer (int*), the size (size_t), and a name (std::string) to help track which buffer is which during operations.

    Declare the following:

    • A constructor that takes a std::string name and size_t size, allocates the array, and prints: [name] constructed with size [size]
    • A move constructor that takes an rvalue reference, steals the resources, and prints: [name] moved from [source_name] (where the moved-to buffer takes the source's name)
    • A destructor that prints [name] destroyed (or empty destroyed if the buffer was moved from)
    • A getSize() method that returns the current size
    • A getName() method that returns the buffer's name

    Remember to mark your move constructor as noexcept and leave the source object in a valid empty state (nullptr, size 0, name "empty").

  • DataBuffer.cpp: Implement all the methods declared in your header. When the destructor runs, only delete the data if the pointer isn't null. Include <iostream> for output.
  • main.cpp: Read two inputs:
    1. A name for your buffer (string)
    2. A size for your buffer (integer)

    Create a DataBuffer with the given name and size. Then create a second buffer by moving from the first using std::move(). After the move, print the state of both buffers:

    • Original: [name] size=[size]
    • New: [name] size=[size]

    Include <utility> for std::move.

For example, with inputs Alpha and 100:

Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyed

With inputs Buffer and 50:

Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyed

Notice how the move constructor transfers ownership of the allocated memory without copying any data. The original buffer is left in an empty but valid state, and when both buffers are destroyed at the end of the program, only the one that still owns the memory actually deletes it.

Cheat sheet

C++ expressions are either lvalues (persistent, addressable) or rvalues (temporary). Move semantics optimize performance by transferring resources from temporary objects instead of copying them.

Rvalue References

Declared with &&, rvalue references bind to temporary objects:

Buffer(Buffer&& other)  // Rvalue reference parameter

Move Constructor

Steals resources from a temporary object. Mark it noexcept and leave the source in a valid empty state:

Buffer(Buffer&& other) noexcept 
    : data(other.data), size(other.size) {
    other.data = nullptr;  // Leave source valid
    other.size = 0;
}

std::move

Casts an lvalue to an rvalue reference, signaling willingness to transfer resources. Include <utility>:

Buffer b1(1000);
Buffer b2(std::move(b1));  // Triggers move constructor

std::move doesn't move anything itself - the actual transfer happens in the move constructor or move assignment operator.

Benefits

Move semantics provide constant-time resource transfer regardless of object size, avoiding expensive deep copies of large data structures.

Try it yourself

#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"

int main() {
    std::string name;
    int size;
    
    std::cin >> name;
    std::cin >> size;
    
    // TODO: Create a DataBuffer with the given name and size
    
    // TODO: Create a second buffer by moving from the first using std::move()
    
    // TODO: Print the state of both buffers:
    // Original: [name] size=[size]
    // New: [name] size=[size]
    
    return 0;
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming