Class yang mengelola resource di heap — buffer, file, koneksi — membayar mahal setiap kali disalin: memori baru dialokasi, isi diduplikasi byte demi byte. Move constructor adalah jawaban C++ untuk situasi pemilik lama sudah tidak membutuhkan resource-nya: alih-alih menduplikasi, objek baru cukup mengambil alih pointer resource, dan objek lama dilepas dari tanggung jawabnya. Biayanya konstan — sebesar menyalin dua pointer — tidak peduli resource itu 10 byte atau 10 juta elemen. Di chapter ini kita menulis move constructor secara manual pada class ber-resource, membuktikan perbedaannya dengan copy constructor, dan membahas keadaan objek sumber setelah dipindahkan.
Anatomi Move Constructor #
Bentuknya mirip copy constructor, tetapi parameternya rvalue reference dan biasanya ditandai noexcept:
Buffer(Buffer&& lain) noexcept;Tiga tugas yang harus dilakukan isinya, dalam urutan yang wajar: (1) pindahkan pointer resource milik lain ke anggota objek baru; (2) nol-kan pointer milik lain supaya destructor objek lama tidak membebaskan resource yang sudah bukan miliknya; (3) tandai fungsi noexcept — alasan pentingnya kita lihat sebentar lagi di chapter Move Assignment. Kontras dengan copy constructor yang mengalokasi buffer baru dan menyalin seluruh isi, move constructor tidak menyentuh isi sama sekali.
Program: Copy versus Move pada Class Ber-Buffer #
Class Buffer di bawah memiliki resource mentah (int*) dan mencetak jejak setiap operasi, sehingga perbedaan biaya copy dan move terbaca langsung dari output:
// move-constructor.cpp — transfer resource lewat move constructor
// Kompilasi: g++ -std=c++23 -Wall -Wextra move-constructor.cpp -o move-constructor
#include <algorithm>
#include <cstddef>
#include <print>
#include <utility>
class Buffer {
public:
explicit Buffer(std::size_t n) : data_(new int[n]{}), ukuran_(n) {
std::println("[ctor ] alokasi {} elemen", ukuran_);
}
// Copy constructor: duplikasi penuh, biaya sebanding ukuran
Buffer(const Buffer& lain)
: data_(new int[lain.ukuran_]), ukuran_(lain.ukuran_) {
std::copy(lain.data_, lain.data_ + ukuran_, data_);
std::println("[copy ] salin {} elemen satu per satu", ukuran_);
}
// Move constructor: transfer pointer, biaya konstan
Buffer(Buffer&& lain) noexcept
: data_(lain.data_), ukuran_(lain.ukuran_) {
lain.data_ = nullptr; // sumber dijaga valid (kosong)
lain.ukuran_ = 0;
std::println("[move ] transfer pointer, sumber jadi kosong");
}
~Buffer() { delete[] data_; } // aman walau data_ == nullptr
std::size_t ukuran() const { return ukuran_; }
int& elemen(std::size_t i) { return data_[i]; }
int elemen(std::size_t i) const { return data_[i]; }
private:
int* data_;
std::size_t ukuran_;
};
Buffer buatBuffer() {
Buffer b(3);
b.elemen(0) = 10;
b.elemen(1) = 20;
b.elemen(2) = 30;
return b; // dibangun langsung di tujuan (NRVO)
}
int main() {
std::println("-- salin: buffer baru dialokasi lalu diisi ulang");
Buffer asal(2);
asal.elemen(0) = 7;
asal.elemen(1) = 42;
Buffer salinan = asal; // copy constructor
std::println("-- pindah: pointer ditransfer, tidak ada salinan elemen");
Buffer tujuan = std::move(asal); // move constructor
std::println("asal.ukuran()={} salinan.elemen(1)={} tujuan.elemen(1)={}",
asal.ukuran(), salinan.elemen(1), tujuan.elemen(1));
std::println("-- kembalikan by value: dibangun langsung di tujuan");
Buffer hasil = buatBuffer();
std::println("hasil.elemen() = {} {} {}",
hasil.elemen(0), hasil.elemen(1), hasil.elemen(2));
return 0;
}Output yang Dihasilkan:
-- salin: buffer baru dialokasi lalu diisi ulang
[ctor ] alokasi 2 elemen
[copy ] salin 2 elemen satu per satu
-- pindah: pointer ditransfer, tidak ada salinan elemen
[move ] transfer pointer, sumber jadi kosong
asal.ukuran()=0 salinan.elemen(1)=42 tujuan.elemen(1)=42
-- kembalikan by value: dibangun langsung di tujuan
[ctor ] alokasi 3 elemen
hasil.elemen() = 10 20 30Mari kita bedah:
- Baris 11-13 (constructor utama): alokasi
nelemen dengannew int[n]{}— label[ctor ]menandai berapa kali resource benar-benar dialokasi. - Baris 16-20 (copy constructor): mengalokasi buffer baru dengan ukuran yang sama lalu
std::copymenyalin seluruh isi. Label[copy ]mencantumkan jumlah elemen — biayanya tumbuh sebanding ukuran data. - Baris 23-28 (move constructor): initializer list memindahkan
lain.data_ke anggota baru (hanya copy pointer), dan badan fungsi menol-kanlain.data_sertalain.ukuran_. Label[move ]tidak menyebut jumlah elemen apa pun — karena memang tidak ada elemen yang disentuh. - Baris 30 (destructor):
delete[] data_aman dipanggil dengan pointernullptr, jadi objek sumber yang sudah dikosongkan tetap berakhir dengan rapi. - Baris 54 (
Buffer salinan = asal;):asallvalue → copy constructor terpilih. Ceksalinan.elemen(1)mengembalikan42, bukti duplikasinya lengkap. - Baris 57-59 (
Buffer tujuan = std::move(asal);):std::movemengubahasalmenjadi rvalue → move constructor terpilih. Setelahnyaasal.ukuran()bernilai0, sedangkantujuan.elemen(1)tetap42— resource pindah utuh tanpa satu elemen pun disalin. - Baris 62-64 (
Buffer hasil = buatBuffer();): fungsi mengembalikan objek by value dan hanya ada satu label[ctor ]— compiler membangun objek langsung dihasil. Mekanismenya (elision) dibahas penuh di chapter Copy Elision.
Moved-from: Valid tapi Tak Terentu #
Keadaan objek sumber setelah dipindahkan punya nama resmi: moved-from state. Standar menjamin dua hal saja — objek itu tetap valid (destructornya boleh berjalan, operator= boleh dipanggil untuk memberinya nilai baru) dan isinya tak terentu (unspecified) sampai kita menetapkannya ulang. Class Buffer di atas memilih menol-kan sumbernya secara eksplisit; std::string hasil pindahan biasanya kosong, tetapi “biasanya” bukan jaminan standar. Konsekuensi praktisnya: setelah std::move(a), kita boleh menugaskan ulang a, tetapi jangan membaca isinya dengan asumsi apa pun.
Satu tanda noexcept di baris 23 belum kita bayar utasnya: realokasi std::vector hanya berani memindahkan elemen bila move-nya dijamin tidak melempar exception. Pembuktian lengkapnya menunggu move assignment di chapter berikutnya.
Rujukan resmi: cppreference — move constructor .