Move semantics (sejak C++11) memungkinkan transfer resource antar objek tanpa menyalin data. Alih-alih menyalin 1 juta elemen int dari satu vector ke yang lain (operasi O(n) dengan alokasi heap besar), kita cukup menukar pointer internal — operasi O(1). Untuk DSA, ini berarti mengembalikan container besar dari fungsi, memindahkan node pohon, atau menyimpan string ke vector bisa dilakukan tanpa copy mahal. Pada Chapter ini kita akan membedah rvalue reference, std::move, move constructor/assignment, dan benchmark copy vs move pada buffer berukuran 4 MB.

Komponen Utama #

KomponenSintaksFungsi
Rvalue referenceT&&Referensi yang hanya mengikat ke rvalue (mis. sementara atau std::move-ed)
std::move(x)Cast x ke T&&Menandai x siap dipindahkan
Move constructorT(T&&)Membangun objek dengan mencuri resource dari sumber
Move assignmentT& operator=(T&&)Mencuri resource, melepaskan resource lama

Mekanisme: lvalue, rvalue, dan Resource Stealing #

  • lvalue: objek dengan alamat (variabel, *ptr, arr[i]). Bisa di-assign.
  • rvalue: objek sementara tanpa nama (literal, hasil fungsi, std::move(x)).
  • Referensi T& mengikat lvalue; T&& mengikat rvalue. Overload T& vs T&& memilih konstruktor/assignment mana yang dipanggil.

Saat kita menulis Buffer c = std::move(a), compiler memilih move constructor. Move constructor menukar pointer internal: c.data_ = a.data_ (steal), lalu a.data_ = nullptr (mencegah double-free). Resource 1 juta elemen dipindahkan tanpa menyalin satu elemen pun.

std::move sendiri tidak memindahkan apa pun — ia hanya cast x ke rvalue reference. Pemindahan resource terjadi di move constructor/assignment yang dipilih karena cast tersebut.

Kode: Buffer dengan Copy dan Move #

cpp
#include <iostream>
#include <vector>
#include <string>
#include <utility>
#include <chrono>

class Buffer {
    size_t size_;
    int* data_;
public:
    // Constructor
    Buffer(size_t n) : size_(n), data_(new int[n]) {
        std::cout << "  ctor size=" << size_ << '\n';
    }
    // Destructor
    ~Buffer() {
        delete[] data_;
        std::cout << "  dtor size=" << size_ << '\n';
    }
    // Copy constructor (deep copy — mahal)
    Buffer(const Buffer& o) : size_(o.size_), data_(new int[o.size_]) {
        for (size_t i = 0; i < size_; ++i) data_[i] = o.data_[i];
        std::cout << "  copy ctor size=" << size_ << '\n';
    }
    // Copy assignment
    Buffer& operator=(const Buffer& o) {
        if (this != &o) {
            delete[] data_;
            size_ = o.size_;
            data_ = new int[size_];
            for (size_t i = 0; i < size_; ++i) data_[i] = o.data_[i];
        }
        std::cout << "  copy assign size=" << size_ << '\n';
        return *this;
    }
    // Move constructor (steal — murah)
    Buffer(Buffer&& o) noexcept : size_(o.size_), data_(o.data_) {
        o.size_ = 0;
        o.data_ = nullptr;
        std::cout << "  move ctor size=" << size_ << '\n';
    }
    // Move assignment
    Buffer& operator=(Buffer&& o) noexcept {
        if (this != &o) {
            delete[] data_;
            size_ = o.size_;
            data_ = o.data_;
            o.size_ = 0;
            o.data_ = nullptr;
        }
        std::cout << "  move assign size=" << size_ << '\n';
        return *this;
    }
    size_t size() const { return size_; }
};

Buffer make_buffer(size_t n) {
    Buffer b(n);
    return b;  // RVO/move
}

int main() {
    std::cout << "--- 1) copy ctor ---\n";
    Buffer a(100);
    Buffer b = a;   // copy ctor

    std::cout << "\n--- 2) move ctor ---\n";
    Buffer c = std::move(a);   // move ctor — a sekarang kosong
    std::cout << "a.size()=" << a.size() << ", c.size()=" << c.size() << '\n';

    std::cout << "\n--- 3) return value (NRVO/move) ---\n";
    Buffer d = make_buffer(50);
    std::cout << "d.size()=" << d.size() << '\n';

    std::cout << "\n--- 4) push_back ke vector ---\n";
    std::vector<Buffer> v;
    v.reserve(3);
    Buffer tmp(10);
    v.push_back(tmp);              // copy (lvalue)
    v.push_back(std::move(tmp));   // move (rvalue)

    std::cout << "\n--- 5) benchmark copy vs move ---\n";
    const size_t N = 1'000'000;
    Buffer big(N);

    auto t1 = std::chrono::high_resolution_clock::now();
    Buffer copy_buf = big;     // deep copy
    auto t2 = std::chrono::high_resolution_clock::now();

    auto t3 = std::chrono::high_resolution_clock::now();
    Buffer move_buf = std::move(big);  // move — hanya pointer swap
    auto t4 = std::chrono::high_resolution_clock::now();

    std::chrono::duration<double, std::micro> dc = t2 - t1;
    std::chrono::duration<double, std::micro> dm = t4 - t3;
    std::cout << "copy 1M int: " << dc.count() << " us\n";
    std::cout << "move 1M int: " << dm.count() << " us\n";

    return 0;
}

Output yang Dihasilkan (running lokal):

text
--- 1) copy ctor ---
  ctor size=100
  copy ctor size=100

--- 2) move ctor ---
  move ctor size=100
a.size()=0, c.size()=100

--- 3) return value (NRVO/move) ---
  ctor size=50
d.size()=50

--- 4) push_back ke vector ---
  ctor size=10
  copy ctor size=10
  move ctor size=10

--- 5) benchmark copy vs move ---
  ctor size=1000000
  copy ctor size=1000000
  move ctor size=1000000
copy 1M int: 1705.15 us
move 1M int: 0.215 us
  dtor size=1000000
  dtor size=1000000
  dtor size=0
  dtor size=0
  dtor size=10
  dtor size=10
  dtor size=50
  dtor size=100
  dtor size=100
  dtor size=0

Bedah Kode Baris-per-Baris #

  1. Buffer b = a (copy ctor): parameter a adalah lvalue → copy constructor dipilih. Buffer b mengalokasikan array baru dan menyalin 100 elemen. a tetap utuh.
  2. Buffer c = std::move(a) (move ctor): std::move(a) meng-cast a ke Buffer&& → move constructor dipilih. c.data_ menerima pointer a.data_; a.data_ = nullptr, a.size_ = 0. Tidak ada alokasi/memcpy. Setelah move, a berada dalam valid but unspecified state — aman untuk destruct atau re-assign.
  3. make_buffer(50): fungsi mengembalikan Buffer by-value. Karena b adalah local lvalue, kita sebut NRVO mungkin, atau move constructor dipilih bila RVO tidak terjadi. Output menunjukkan satu ctor (di dalam fungsi) — RVO menghilangkan move. Ini optimalisasi compiler, bukan perilaku wajib.
  4. v.reserve(3): memesan kapasitas 3 sehingga push_back berikutnya tidak memicu realokasi (yang akan mem-move seluruh elemen).
  5. v.push_back(tmp): tmp adalah lvalue → copy constructor. Resource tmp tetap utuh.
  6. v.push_back(std::move(tmp)): tmp di-cast ke rvalue → move constructor. Resource dipindahkan; tmp sekarang kosong tetapi masih hidup sampai akhir scope.
  7. Benchmark Buffer copy_buf = big: copy ctor mengalokasikan array 1M int (~4 MB) lalu memcpy. Waktu ~1700 µs (1.7 ms) — sekitar 2.3 GB/s (konstan memcpy modern).
  8. Benchmark Buffer move_buf = std::move(big): move ctor hanya menyalin dua pointer (size_, data_) dan men-nolkan sumbernya. Waktu ~0.2 µs — 8000× lebih cepat dari copy.

Move di STL Container #

Container STL sudah mendukung move sejak C++11:

  • std::vector::push_back(T&&) — move constructor elemen dipanggil bila argumen adalah rvalue.
  • std::vector::insert overload dengan T&&.
  • std::string, std::vector, std::map, dst. punya move constructor noexcept.

Move membuat operasi seperti v2 = std::move(v1) sangat murah — hanya swap pointer internal. Untuk container berukuran besar, ini mengubah O(n) copy menjadi O(1) move.

noexcept Move Constructor #

Move constructor sebaiknya selalu noexcept:

cpp
Buffer(Buffer&& o) noexcept { ... }

std::vector saat realokasi memeriksa apakah move ctor noexcept — bila ya, ia pakai move; bila tidak, ia pakai copy untuk menjaga strong exception guarantee. Tanpa noexcept, Anda kehilangan benefit move pada vector::reserve/push_back saat realokasi.

Pola Umum di DSA #

Sink Function #

cpp
void push_value(std::vector<int>& v, int x) {
    v.push_back(x);  // copy OK untuk int
}
void push_value(std::vector<std::string>& v, std::string s) {
    v.push_back(std::move(s));  // move string
}

Return Large Object #

cpp
std::vector<int> compute(int n) {
    std::vector<int> result;
    result.reserve(n);
    // ... isi result ...
    return result;  // RVO atau move
}

Move-Only Type #

std::unique_ptr tidak bisa di-copy; hanya bisa di-move. Ini memastikan kepemilikan eksklusif:

cpp
auto p = std::make_unique<int>(42);
auto q = std::move(p);  // p sekarang nullptr

Ringkasan #

  • Rvalue reference T&& + std::move(x) memilih move constructor/assignment.
  • Move constructor “mencuri” resource (pointer swap), mengubah O(n) copy menjadi O(1) move.
  • std::move hanya cast; pemindahan resource terjadi di move ctor/assignment.
  • Setelah std::move(x), x valid tetapi tidak terdefinisi nilainya — hanya destruct atau re-assign yang aman.
  • Selalu tandai move ctor/assignment noexcept agar STL memakai move saat realokasi.
  • Pola sink: void set(T x) { member_ = std::move(x); } — satu overload melayani copy dan move.