Command Palette

Search for a command to run...

MTH 4300

Lecture 11

Lvalues and Rvalues

Every expression in C++ is either an lvalue or an rvalue:

  • lvalue — has a name and a persistent address. Can appear on the left side of =.
  • rvalue — a temporary with no name. It exists only for the duration of the expression.
lvalue vs rvalue
int x = 5;        // x is an lvalue; 5 is an rvalue
String s("hi");   // s is an lvalue; String("hi") is an rvalue (temporary)
int y = x + 1;    // x is an lvalue; x+1 is an rvalue (temporary result)

When you copy a temporary into a new object, the temporary is about to be destroyed anyway — the copy is unnecessary work. Move semantics let you steal the temporary's resources instead.

Rvalue References

An rvalue reference (&&) binds only to rvalues. It lets you write overloads that the compiler selects specifically when working with temporaries:

Rvalue reference
void process(const String& s) { /* copy path — lvalue */ }
void process(String&& s)      { /* move path — rvalue/temporary */ }

String a("hi");
process(a);             // lvalue → copy overload
process(String("hi"));  // rvalue → move overload

Move Constructor

The move constructor takes an rvalue reference and steals the source's resources:

Move constructor
String::String(String&& other) noexcept
    : len_(other.len_), data_(other.data_) {
    other.len_  = 0;        // leave source in a valid but empty state
    other.data_ = nullptr;  // prevent double-free when source is destroyed
}

Two things happen:

  1. this takes ownership of other's buffer — no heap allocation needed.
  2. other's members are zeroed so its destructor (delete[] nullptr) is a no-op.

noexcept tells the compiler this operation cannot throw. Standard containers like std::vector require it to choose the move constructor over the copy constructor during reallocation.

Move Assignment Operator

Like copy assignment, but steals instead of deep-copying:

Move assignment operator
String& String::operator=(String&& other) noexcept {
    if (this == &other) return *this;

    delete[] data_;         // free current resource

    len_  = other.len_;
    data_ = other.data_;

    other.len_  = 0;
    other.data_ = nullptr;

    return *this;
}

std::move

An lvalue can be explicitly cast to an rvalue reference with std::move. This signals: "I am done with this object; you may steal from it."

std::move
#include <utility>

String a("hello");
String b = std::move(a); // move constructor: b steals a's buffer; a becomes empty

std::cout << b.c_str() << "\n"; // "hello"
std::cout << a.size()   << "\n"; // 0 — a was moved from

After std::move(a), a is in a valid but unspecified state. You can destroy it or assign a new value to it, but you must not read from it first. Using a moved-from object is a common source of bugs.

std::move does not move anything — it just performs the cast. The actual resource transfer happens inside the move constructor or move assignment operator.

The Rule of Five

The Rule of Three from Lecture 10 expands to the Rule of Five in modern C++:

If you define any of these, define all five:

  1. Destructor
  2. Copy constructor
  3. Copy assignment operator
  4. Move constructor
  5. Move assignment operator

Omitting the move operations does not break correctness — the compiler falls back to copy — but it leaves performance on the table everywhere the compiler could have moved instead.

String with Full Move Semantics

String — declaration after Lecture 11
class String {
public:
    String();
    explicit String(const char* s);
    ~String();

    String(const String& other);
    String& operator=(const String& other);

    String(String&& other) noexcept;            // move constructor
    String& operator=(String&& other) noexcept; // move assignment

    size_t size()  const;
    bool   empty() const;
    char   at(size_t i) const;
    const char* c_str() const;

private:
    size_t len_;
    char*  data_;
};

Exercises

Q1.

Without running the code, predict what this program prints.

#include <iostream>
#include <utility>

int main() {
    String a("hello");

    String b = a;              // line A
    String c = std::move(a);   // line B

    std::cout << b.c_str() << "\n"; // line C
    std::cout << c.c_str() << "\n"; // line D
    std::cout << a.size()   << "\n"; // line E

    return 0;
}

Q2.

Without running the code, predict what this program prints.

#include <iostream>
#include <utility>

int main() {
    String a("foo");
    String b("bar");

    b = std::move(a); // line A — move assignment

    std::cout << b.c_str() << "\n"; // line B
    std::cout << a.size()   << "\n"; // line C

    a = String("baz"); // line D — move assignment from temporary
    std::cout << a.c_str() << "\n"; // line E

    return 0;
}

Q3.

Each snippet has a bug related to move semantics. Identify what's wrong.

// Snippet A — move constructor without nulling source
String::String(String&& other) noexcept
    : len_(other.len_), data_(other.data_) {
    // missing: other.data_ = nullptr; other.len_ = 0;
}

// Snippet B — using a moved-from object
String a("hello");
String b = std::move(a);
std::cout << a.c_str() << "\n"; // reads a after it was moved from

// Snippet C — move assignment without freeing old resource
String& String::operator=(String&& other) noexcept {
    len_  = other.len_;
    data_ = other.data_;
    other.len_  = 0;
    other.data_ = nullptr;
    return *this;
}

// Snippet D — move constructor missing noexcept
String::String(String&& other) // no noexcept
    : len_(other.len_), data_(other.data_) {
    other.len_  = 0;
    other.data_ = nullptr;
}

Q4.

Implement the move constructor and move assignment operator for String.

String::String(String&& other) noexcept {
    // TODO: steal other's resources; leave other valid and empty
}

String& String::operator=(String&& other) noexcept {
    // TODO: guard, free old buffer, steal other's resources, null other
}

Practice Problems