Move constructor mengurus objek yang baru lahir; move assignment mengurus objek yang sudah punya resource. Tugas tambahan inilah yang membuat penulisannya lebih hati-hati: sebelum mengambil alih resource milik sumber, objek tujuan wajib membebaskan resource lamanya sendiri — kalau tidak, pointer lama tertimpa dan memori bocor. Ada satu kasus jebakan klasik yang hanya muncul pada assignment: sumber dan tujuan bisa jadi objek yang sama (self-assignment), dan tanpa pengaman, urutan “lepas dulu, ambil alih kemudian” justru membebaskan resource yang hendak dipindahkan. Chapter ini membangun operator=(T&&) noexcept yang benar langkah demi langkah, lalu membuktikan aturan noexcept yang menentukan cara std::vector tumbuh.

Tiga Langkah Move Assignment yang Benar #

Signature-nya sejajar dengan move constructor: Kotak& operator=(Kotak&& lain) noexcept. Badan fungsi yang benar selalu memuat tiga langkah berurutan:

  1. Guard self-assignment: bila this == &lain, selesaikan tanpa menyentuh apa pun. Kasus ini sah menurut standar dan muncul ketika pemanggil tidak bisa memastikan bahwa sumber dan tujuan berbeda — misalnya data[i] = std::move(data[j]) dengan i == j.
  2. Lepas resource lama: delete[] buffer milik objek tujuan sebelum pointernya ditimpa.
  3. Ambil alih resource sumber dan kosongkan sumber: pointer dipindahkan, sumber dijaga dalam keadaan valid-tapi-kosong, lalu kembalikan *this.

Tanda noexcept pada baris signature bukan formalitas: ia adalah data yang dibaca container standar untuk memutuskan apakah memindahkan elemen itu aman. Pembuktiannya menyusul di bagian akhir chapter.

Program: Guard, Lepas Lama, Ambil Alih #

Class Kotak memakai resource mentah (char*) agar setiap langkah terlihat eksplisit:

cpp
// move-assignment.cpp — operator=(T&&): lepas resource lama, jaga self-assignment
// Kompilasi: g++ -std=c++23 -Wall -Wextra move-assignment.cpp -o move-assignment

#include <cstddef>
#include <cstring>
#include <print>
#include <utility>
#include <vector>

class Kotak {
public:
    explicit Kotak(const char* isi)
        : panjang_(std::strlen(isi)), isi_(new char[panjang_ + 1]) {
        std::memcpy(isi_, isi, panjang_ + 1);
        std::println("[ctor ] '{}' dialokasi", isi_);
    }

    Kotak(const Kotak& lain)
        : panjang_(lain.panjang_), isi_(new char[lain.panjang_ + 1]) {
        std::memcpy(isi_, lain.isi_, panjang_ + 1);
        std::println("[copy ] '{}' disalin ke buffer baru", isi_);
    }

    Kotak(Kotak&& lain) noexcept
        : panjang_(lain.panjang_), isi_(lain.isi_) {
        lain.isi_ = nullptr;
        lain.panjang_ = 0;
        std::println("[move ] pointer diambil alih");
    }

    // Copy assignment: resource lama dilepas dulu, lalu isi baru dialokasi
    Kotak& operator=(const Kotak& lain) {
        if (this != &lain) {
            delete[] isi_;
            panjang_ = lain.panjang_;
            isi_ = new char[panjang_ + 1];
            std::memcpy(isi_, lain.isi_, panjang_ + 1);
            std::println("[copy= ] resource lama dilepas, isi baru disalin");
        }
        return *this;
    }

    // Move assignment: noexcept, guard self-assignment, lepas resource lama
    Kotak& operator=(Kotak&& lain) noexcept {
        if (this == &lain) {                 // guard self-assignment
            std::println("[move= ] self-assignment dilewati dengan aman");
            return *this;
        }
        delete[] isi_;                       // lepas resource lama dulu
        isi_ = lain.isi_;                    // ambil alih resource sumber
        panjang_ = lain.panjang_;
        lain.isi_ = nullptr;                 // sumber tetap valid (kosong)
        lain.panjang_ = 0;
        std::println("[move= ] resource lama dilepas, pointer diambil alih");
        return *this;
    }

    ~Kotak() { delete[] isi_; }

    const char* isi() const { return isi_ == nullptr ? "" : isi_; }
    std::size_t panjang() const { return panjang_; }

private:
    std::size_t panjang_;   // dideklarasikan dulu agar dialokasi sebelum isi_
    char* isi_;
};

int main() {
    std::println("-- move assignment: resource lama dilepas dulu");
    Kotak a("lama");
    Kotak b("baru");
    b = std::move(a);              // buffer "baru" dibebaskan, pointer "lama" pindah
    std::println("b.isi()='{}' ({} byte) | a.panjang()={}",
                 b.isi(), b.panjang(), a.panjang());

    std::println("-- self-assignment: sumber dan tujuan bisa jadi objek yang sama");
    Kotak& sumber = b;             // pemanggil tidak selalu tahu sumber == tujuan
    b = std::move(sumber);         // (mis. elemen array dengan indeks sama)
    std::println("b.isi() masih '{}' (utuh, guard bekerja)", b.isi());

    std::println("-- noexcept move: realokasi std::vector memindahkan elemen");
    std::vector<Kotak> tumpukan;
    tumpukan.reserve(2);
    tumpukan.push_back(Kotak("satu"));
    tumpukan.push_back(Kotak("dua"));
    tumpukan.push_back(Kotak("tiga"));   // kapasitas penuh: elemen lama direlokasi
    std::println("ukuran vector = {}", tumpukan.size());
    return 0;
}

Output yang Dihasilkan:

text
-- move assignment: resource lama dilepas dulu
[ctor ] 'lama' dialokasi
[ctor ] 'baru' dialokasi
[move= ] resource lama dilepas, pointer diambil alih
b.isi()='lama' (4 byte) | a.panjang()=0
-- self-assignment: sumber dan tujuan bisa jadi objek yang sama
[move= ] self-assignment dilewati dengan aman
b.isi() masih 'lama' (utuh, guard bekerja)
-- noexcept move: realokasi std::vector memindahkan elemen
[ctor ] 'satu' dialokasi
[move ] pointer diambil alih
[ctor ] 'dua' dialokasi
[move ] pointer diambil alih
[ctor ] 'tiga' dialokasi
[move ] pointer diambil alih
[move ] pointer diambil alih
[move ] pointer diambil alih
ukuran vector = 3

Mari kita bedah:

  1. Baris 44-56 (move assignment): urutan badan fungsi memperagakan tiga langkah di atas — guard di baris 45-48, delete[] isi_ di baris 49 sebelum pointer ditimpa, lalu pengambilalihan pointer dan pengosongan sumber di baris 50-53.
  2. Baris 72 (b = std::move(a);): buffer milik b yang berisi "baru" dibebaskan terlebih dahulu, lalu pointer milik a berpindah. Baris 73-74 membuktikan hasilnya: b kini memegang "lama", sedangkan a.panjang() kembali 0.
  3. Baris 77-79 (self-assignment): sumber adalah referensi ke b, sehingga b = std::move(sumber) adalah self-assignment yang tidak terlihat compiler. Guard menangkapnya — label [move= ] self-assignment dilewati tercetak dan isi b tetap utuh.
  4. Baris 84-86 (realokasi vector): reserve(2) membatasi kapasitas; push_back ketiga memaksa std::vector pindah ke buffer lebih besar. Dua label [move ] yang muncul setelah [ctor ] 'tiga' adalah relokasi dua elemen lama — karena move constructor Kotak di baris 24 bertanda noexcept, elemen lama dipindahkan, bukan disalin; total lima label [move ] pada bagian ini mencakup dua relokasi itu plus tiga penyisipan elemen baru.
  5. Baris 64-65 (urutan anggota): panjang_ dideklarasikan sebelum isi_ karena inisialisasi anggota mengikuti urutan deklarasi, bukan urutan di initializer list — isi_ memakai panjang_ + 1 untuk ukuran alokasinya.

Kasus self-assignment yang ditulis eksplisit (b = std::move(b);) sebenarnya sudah dikenali compiler modern. GCC 14 memperingatkannya lewat -Wself-move:

text
ch5-selfmove.cpp: In function ‘int main()’:
ch5-selfmove.cpp:17:7: warning: moving ‘b’ of type ‘Kotak’ to itself [-Wself-move]
   17 |     b = std::move(b);   // self-move: compiler memperingatkan
      |     ~~^~~~~~~~~~~~~~
ch5-selfmove.cpp:17:7: note: remove ‘std::move’ call

Peringatan itu membantu untuk kasus yang terlihat jelas, tetapi guard di baris 45 tetap wajib ada: pada kasus melalui referensi atau indeks array, compiler tidak bisa mengetahui bahwa sumber dan tujuan adalah objek yang sama.

noexcept Menentukan Cara std::vector Tumbuh #

Saat realokasi, std::vector harus memindahkan elemen-elemen lama ke buffer baru. Pemindahan itu berisiko: bila move constructor ke-n melempar exception, elemen-elemen yang sudah dipindahkan tidak bisa dikembalikan, dan invarian container pecah. Karena itu standar memerintahkan std::vector memakai strategi move if noexcept (std::move_if_noexcept): gunakan move hanya bila move constructor dijamin tidak melempar; bila tidak, mundur ke copy yang bisa diputar ulang dengan aman. Class berikut mengukur pilihan itu dengan penghitung statis:

cpp
// realokasi-vector.cpp — noexcept move menentukan pilihan std::vector saat tumbuh
// Kompilasi: g++ -std=c++23 -Wall -Wextra realokasi-vector.cpp -o realokasi-vector

#include <print>
#include <string>
#include <utility>
#include <vector>

// Kelas dengan move constructor noexcept
class Aman {
public:
    Aman() = default;
    explicit Aman(std::string isi) : isi_(std::move(isi)) {}
    Aman(const Aman& lain) : isi_(lain.isi_) { ++salinan; }
    Aman(Aman&& lain) noexcept : isi_(std::move(lain.isi_)) { ++pindahan; }
    Aman& operator=(const Aman&) = default;
    Aman& operator=(Aman&&) noexcept = default;

    static void reset() { salinan = 0; pindahan = 0; }

    inline static int salinan = 0;
    inline static int pindahan = 0;

private:
    std::string isi_;
};

// Kelas dengan move constructor yang TIDAK noexcept
class Berisiko {
public:
    Berisiko() = default;
    explicit Berisiko(std::string isi) : isi_(std::move(isi)) {}
    Berisiko(const Berisiko& lain) : isi_(lain.isi_) { ++salinan; }
    Berisiko(Berisiko&& lain) : isi_(std::move(lain.isi_)) { ++pindahan; }  // tanpa noexcept
    Berisiko& operator=(const Berisiko&) = default;
    Berisiko& operator=(Berisiko&&) = default;

    static void reset() { salinan = 0; pindahan = 0; }

    inline static int salinan = 0;
    inline static int pindahan = 0;

private:
    std::string isi_;
};

template <typename T>
void ujiRealokasi(const char* nama) {
    std::vector<T> v;
    v.reserve(2);                  // kapasitas 2 elemen
    v.push_back(T("satu"));
    v.push_back(T("dua"));
    T::reset();                    // hitung hanya proses realokasi + masuknya elemen baru
    v.push_back(T("tiga"));        // kapasitas penuh: elemen lama dipindahkan ke buffer baru
    std::println("{}: realokasi -> salinan={} pindahan={}",
                 nama, T::salinan, T::pindahan);
}

int main() {
    std::println("perbandingan realokasi std::vector (reserve 2, push elemen ke-3)");
    ujiRealokasi<Aman>("move noexcept     ");
    ujiRealokasi<Berisiko>("move tanpa noexcept");
    return 0;
}

Output yang Dihasilkan:

text
perbandingan realokasi std::vector (reserve 2, push elemen ke-3)
move noexcept     : realokasi -> salinan=0 pindahan=3
move tanpa noexcept: realokasi -> salinan=2 pindahan=1

Perbedaannya tegas. Class Aman (move noexcept di baris 15) menghasilkan salinan=0 pindahan=3 — dua elemen lama direlokasi dengan move, plus satu move untuk memasukkan elemen baru. Class Berisiko (move tanpa noexcept di baris 34) menghasilkan salinan=2 pindahan=1 — realokasi mundur ke copy, dan hanya penyisipan elemen baru yang bergerak. Satu kata kunci mengubah biaya realokasi dari konstan menjadi sebanding ukuran string. Aturan praktisnya: tandai move constructor dan move assignment noexcept setiap kali implementasinya memang tidak melempar.

Dengan move constructor dan move assignment lengkap di tangan, sekarang kita punya mesin pindahannya — tinggal memahami pemicunya. Chapter berikutnya membongkar std::move: fungsi yang namanya paling menyesatkan di seluruh C++.

Rujukan resmi: cppreference — move assignment operator , cppreference — std::move_if_noexcept .