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:

cpp
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:

cpp
// 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:

text
-- 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 30

Mari kita bedah:

  1. Baris 11-13 (constructor utama): alokasi n elemen dengan new int[n]{} — label [ctor ] menandai berapa kali resource benar-benar dialokasi.
  2. Baris 16-20 (copy constructor): mengalokasi buffer baru dengan ukuran yang sama lalu std::copy menyalin seluruh isi. Label [copy ] mencantumkan jumlah elemen — biayanya tumbuh sebanding ukuran data.
  3. Baris 23-28 (move constructor): initializer list memindahkan lain.data_ ke anggota baru (hanya copy pointer), dan badan fungsi menol-kan lain.data_ serta lain.ukuran_. Label [move ] tidak menyebut jumlah elemen apa pun — karena memang tidak ada elemen yang disentuh.
  4. Baris 30 (destructor): delete[] data_ aman dipanggil dengan pointer nullptr, jadi objek sumber yang sudah dikosongkan tetap berakhir dengan rapi.
  5. Baris 54 (Buffer salinan = asal;): asal lvalue → copy constructor terpilih. Cek salinan.elemen(1) mengembalikan 42, bukti duplikasinya lengkap.
  6. Baris 57-59 (Buffer tujuan = std::move(asal);): std::move mengubah asal menjadi rvalue → move constructor terpilih. Setelahnya asal.ukuran() bernilai 0, sedangkan tujuan.elemen(1) tetap 42 — resource pindah utuh tanpa satu elemen pun disalin.
  7. Baris 62-64 (Buffer hasil = buatBuffer();): fungsi mengembalikan objek by value dan hanya ada satu label [ctor ] — compiler membangun objek langsung di hasil. 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 .