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:
- 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 — misalnyadata[i] = std::move(data[j])dengani == j. - Lepas resource lama:
delete[]buffer milik objek tujuan sebelum pointernya ditimpa. - 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:
// 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:
-- 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 = 3Mari kita bedah:
- 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. - Baris 72 (
b = std::move(a);): buffer milikbyang berisi"baru"dibebaskan terlebih dahulu, lalu pointer milikaberpindah. Baris 73-74 membuktikan hasilnya:bkini memegang"lama", sedangkana.panjang()kembali0. - Baris 77-79 (self-assignment):
sumberadalah referensi keb, sehinggab = std::move(sumber)adalah self-assignment yang tidak terlihat compiler. Guard menangkapnya — label[move= ] self-assignment dilewatitercetak dan isibtetap utuh. - Baris 84-86 (realokasi vector):
reserve(2)membatasi kapasitas;push_backketiga memaksastd::vectorpindah ke buffer lebih besar. Dua label[move ]yang muncul setelah[ctor ] 'tiga'adalah relokasi dua elemen lama — karena move constructorKotakdi baris 24 bertandanoexcept, elemen lama dipindahkan, bukan disalin; total lima label[move ]pada bagian ini mencakup dua relokasi itu plus tiga penyisipan elemen baru. - Baris 64-65 (urutan anggota):
panjang_dideklarasikan sebelumisi_karena inisialisasi anggota mengikuti urutan deklarasi, bukan urutan di initializer list —isi_memakaipanjang_ + 1untuk ukuran alokasinya.
Kasus self-assignment yang ditulis eksplisit (b = std::move(b);) sebenarnya sudah dikenali compiler modern. GCC 14 memperingatkannya lewat -Wself-move:
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’ callPeringatan 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:
// 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:
perbandingan realokasi std::vector (reserve 2, push elemen ke-3)
move noexcept : realokasi -> salinan=0 pindahan=3
move tanpa noexcept: realokasi -> salinan=2 pindahan=1Perbedaannya 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 .