EduC++ Move Semantics in C++

Move Semantics in C++

Prerequisites: Value categories (lvalue vs rvalue), Rule of Five, RAII
Standard: C++11 (std::move, rvalue references, move special members)
VALUE CATEGORIES Deep Dive
 

Watch out: std::move does NOT move -- it casts to an rvalue reference. The actual move happens in the move constructor / move assignment operator. -----------------------------------------------

class Buffer {
    char* data_;
    std::size_t size_;
    std::string name_;

public:
    // Constructor
    Buffer(std::string name, std::size_t size)
        : data_(new char[size]), size_(size), name_(std::move(name)) {
        std::memset(data_, 0, size_);
        std::cout << std::format("  [{}] Constructed ({} bytes)\n", name_, size_);
    }

    // Destructor
    ~Buffer() {
        if (data_) {
            std::cout << std::format("  [{}] Destroyed\n", name_);
        }
        delete[] data_;
    }

    // ---- Copy constructor (expensive: allocates + copies) ----
    Buffer(const Buffer& other)
        : data_(new char[other.size_]),
          size_(other.size_),
          name_(other.name_ + " (copy)") {
        std::memcpy(data_, other.data_, size_);
        std::cout << std::format("  [{}] COPIED ({} bytes allocated)\n",
                                  name_, size_);
    }

    // ---- Move constructor (cheap: just pointer swap) ----
    //
STD::MOVE Deep Dive
std::move does NOT move anything.  It is purely a cast:

  template<typename T>
  constexpr std::remove_reference_t<T>&& move(T&& x) noexcept {
      return static_cast<std::remove_reference_t<T>&&>(x);
  }

Conceptually: T&& move(T& x) { return static_cast<T&&>(x); }

All it does is change the value category of 'x' from lvalue to
xvalue (eXpiring value).  This makes the expression eligible for
binding to T&& parameters, so the compiler selects the move
constructor instead of the copy constructor.

The *actual* resource transfer (stealing the pointer, nulling the
source) happens right here in the move constructor that YOU write.

Watch out: after std::move, the source object is in a
"valid but unspecified" state.  The C++ standard only guarantees
that you can safely destroy it or assign a new value to it.
Do NOT read from it expecting any particular contents.
Buffer(Buffer&& other) noexcept
        : data_(other.data_),           // Steal the pointer
          size_(other.size_),
          name_(std::move(other.name_) + " (moved)") {
        other.data_ = nullptr;          // Leave source in valid empty state
        other.size_ = 0;
        std::cout << std::format("  [{}] MOVED (zero allocation)\n", name_);
    }

    // ---- Copy assignment ----
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data_;
            data_ = new char[other.size_];
            size_ = other.size_;
            std::memcpy(data_, other.data_, size_);
            name_ = other.name_ + " (copy=)";
            std::cout << std::format("  [{}] Copy assigned\n", name_);
        }
        return *this;
    }

    // ---- Move assignment ----
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            name_ = std::move(other.name_) + " (move=)";
            other.data_ = nullptr;
            other.size_ = 0;
            std::cout << std::format("  [{}] Move assigned\n", name_);
        }
        return *this;
    }

    [[nodiscard]] std::size_t size() const { return size_; }
    [[nodiscard]] const std::string& name() const { return name_; }
};

2. Functions demonstrating when moves happen

of a local variable -- it inhibits NRVO (named
   return value optimization), making performance
   WORSE, not better.

Watch out: do NOT std::move the return value

Buffer create_buffer() {
    Buffer b("factory", 1024);
    return b;  // NRVO (Named Return Value Optimization) may elide the move
}

void take_ownership(Buffer b) {
    std::cout << std::format("  Took ownership of: {}\n", b.name());
    // b is destroyed at end of function
}

3. std::move with STL containers

Moving strings and vectors avoids deep copies.

   is in a valid-but-unspecified state -- you can
   assign to it or destroy it, but do NOT read
   from it expecting any particular value.

Watch out: after std::move, the source object

void stl_move_demo() {
    std::cout << "\n--- STL Move Semantics ---\n";

    std::string source = "Hello, this is a long string that uses heap memory";
    std::cout << std::format("Before move: source = '{}'\n", source);

    std::string dest = std::move(source);  // Pointer swap, O(1)
    std::cout << std::format("After move:  dest = '{}'\n", dest);
    std::cout << std::format("After move:  source = '{}' (valid but unspecified)\n",
                              source);

    // Moving elements into a vector
    std::vector<std::string> names;
    std::string name = "Alice";
    names.push_back(std::move(name));  // Move instead of copy
    names.emplace_back("Bob");         // Construct in-place (even better)

    std::cout << std::format("names: [{}, {}]\n", names[0], names[1]);
}

4. Perfect forwarding with std::forward

Preserves the value category (lvalue/rvalue) of arguments.
//
// HOW IT WORKS: PERFECT FORWARDING
//
// In the signature  template<typename T> void wrapper(T&& arg),
// T&& is a "forwarding reference" (sometimes called "universal
// reference") -- NOT an rvalue reference -- because T is being
// deduced by the compiler.
//
// Reference collapsing rules determine what T&& becomes:
//
//   If the caller passes an lvalue (e.g., a named variable):
//     T is deduced as int&
//     T&&  =  int& &&  =  int&          (reference collapsing)
//     -> arg is an lvalue reference
//
//   If the caller passes an rvalue (e.g., std::move(x) or a temp):
//     T is deduced as int
//     T&&  =  int&&                      (no collapsing needed)
//     -> arg is an rvalue reference
//
// The problem: inside the function, 'arg' is always an lvalue
// (because it has a name), regardless of what the caller passed.
// If you just wrote  take_ownership(arg);  it would ALWAYS copy.
//
// std::forward<T>(arg) casts 'arg' back to its original category:
//   - If T = int&  :  forward returns int&   (stays lvalue)
//   - If T = int   :  forward returns int&&  (restored to rvalue)
//
// Without std::forward, the rvalue-ness of the original argument
// is lost the moment it binds to the named parameter 'arg'.
// Forward restores it, enabling the callee (take_ownership) to
// select the move constructor when the caller intended a move.
template<typename T>
void wrapper(T&& arg) {
    // std::forward<T> passes lvalues as lvalues, rvalues as rvalues
    take_ownership(std::forward<T>(arg));
}

int main() {
    std::cout << "--- Copy vs Move ---\n";

    Buffer original("orig", 1024);

    // Copy: allocates new memory and copies bytes
    std::cout << "\nCopying:\n";
    Buffer copied = original;

    // Move: just steals the pointer (no allocation!)
    std::cout << "\nMoving:\n";
    Buffer moved = std::move(original);
    // 'original' is now in a valid but empty state

    // Return value optimization
    std::cout << "\n--- Return Value (NRVO) ---\n";
    auto buf = create_buffer();

    // Transfer ownership to a function
    std::cout << "\n--- Transfer Ownership ---\n";
    take_ownership(std::move(buf));

    // STL containers
    stl_move_demo();

    // Perfect forwarding
    std::cout << "\n--- Perfect Forwarding ---\n";
    Buffer fwd_buf("forward-me", 512);
    wrapper(std::move(fwd_buf));  // Forwarded as rvalue -> move

    return 0;
}