Dua belas chapter terakhir membangun satu per satu mesin move semantics: kategori ekspresi, rvalue reference, move constructor, move assignment, std::move, forwarding, collapsing, dan elision. Chapter penutup ini menjawab pertanyaan desain yang mengikat semuanya: untuk class yang kita tulis, fungsi khusus mana yang perlu ditulis sendiri? Jawabannya dirangkum tiga aturan berjenjang — Rule of Three (warisan C++98), Rule of Five (lengkap dengan move), dan Rule of Zero (pilihan paling modern): delegasikan pengelolaan resource ke member yang mengelola dirinya sendiri, sehingga tidak ada satu pun fungsi khusus yang perlu kita tulis. Kita bahas ketiganya dengan kode yang diuji, lalu tutup section ini dengan peta jalan ke bagian berikutnya.

Rule of Three dan Rule of Five #

Rule of Three lahir dari satu prinsip: jika class perlu menulis satu dari tiga fungsi khusus — destructor, copy constructor, atau copy assignment — hampir pasti class itu mengelola resource yang butuh ketiganya. Destructor yang membebaskan buffer berarti copy constructor juga harus menduplikasi buffer (bukan menyalin pointer), dan copy assignment harus melepas buffer lama sebelum menduplikasi. Melanggarnya menghasilkan double-free atau memory leak.

C++11 memperluas daftarnya menjadi lima: ditambah move constructor dan move assignment. Menulis empat tapi melupakan yang kelima membuat class kehilangan jalur pindahan — container akan jatuh ke copy yang lebih lambat, atau lebih buruk, class tidak bisa dipindahkan sama sekali. Class Tugas di program di bawah memperagakan Rule of Five penuh pada resource mentah char*, dengan jejak pada setiap operasi:

cpp
// rule-of-zero-three-five.cpp — Rule of Three, Five, dan Zero dalam satu program
// Kompilasi: g++ -std=c++23 -Wall -Wextra rule-of-zero-three-five.cpp -o rule-of-zero-three-five

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

// --- Rule of Five: resource mentah dikelola sendiri, lima fungsi khusus ditulis ---
class Tugas {
public:
    explicit Tugas(const char* nama)
        : nama_(nama), data_(new char[std::strlen(nama) + 1]) {
        std::memcpy(data_, nama, std::strlen(nama) + 1);
        std::println("[ctor ] '{}' (alokasi buffer)", nama_);
    }

    Tugas(const Tugas& lain)
        : nama_(lain.nama_), data_(new char[std::strlen(lain.data_) + 1]) {
        std::memcpy(data_, lain.data_, std::strlen(lain.data_) + 1);
        std::println("[copy ] '{}' (buffer baru, isi diduplikasi)", nama_);
    }

    Tugas(Tugas&& lain) noexcept
        : nama_(std::move(lain.nama_)), data_(lain.data_) {
        lain.data_ = nullptr;
        std::println("[move ] '{}' (buffer diambil alih, sumber kosong)", nama_);
    }

    Tugas& operator=(const Tugas& lain) {
        if (this != &lain) {
            delete[] data_;
            nama_ = lain.nama_;
            data_ = new char[std::strlen(lain.data_) + 1];
            std::memcpy(data_, lain.data_, std::strlen(lain.data_) + 1);
            std::println("[copy= ] '{}' (resource lama dilepas, isi diduplikasi)", nama_);
        }
        return *this;
    }

    Tugas& operator=(Tugas&& lain) noexcept {
        if (this != &lain) {
            delete[] data_;
            nama_ = std::move(lain.nama_);
            data_ = lain.data_;
            lain.data_ = nullptr;
            std::println("[move= ] '{}' (resource lama dilepas, buffer diambil alih)", nama_);
        }
        return *this;
    }

    ~Tugas() { delete[] data_; }

    const char* data() const { return data_ == nullptr ? "(kosong)" : data_; }

private:
    std::string nama_;
    char* data_;
};

// --- Rule of Zero: member mengelola dirinya, kita tidak menulis apa pun ---
class Profil {
public:
    explicit Profil(std::string nama) : nama_(std::move(nama)) {}
    const std::string& nama() const { return nama_; }
private:
    std::string nama_;   // copy, move, destructor: semua implisit dan benar
};

// --- = delete dan = default: melarang copy, membiarkan move ---
class Sertifikat {
public:
    explicit Sertifikat(std::string kode) : kode_(std::move(kode)) {}
    Sertifikat(const Sertifikat&) = delete;             // copy dilarang
    Sertifikat& operator=(const Sertifikat&) = delete;
    Sertifikat(Sertifikat&&) = default;                 // move tetap tersedia
    Sertifikat& operator=(Sertifikat&&) = default;
    const std::string& kode() const { return kode_; }
private:
    std::string kode_;
};

int main() {
    std::println("-- Rule of Five: lima operasi dengan jejak");
    Tugas t("laporan");
    Tugas s = t;                    // copy constructor
    Tugas m = std::move(t);         // move constructor
    s = m;                          // copy assignment
    m = Tugas("sementara");         // move assignment (dari temporary)
    std::println("m.data()='{}' s.data()='{}'", m.data(), s.data());

    std::println("-- Rule of Zero: nol deklarasi fungsi khusus, perilaku tetap benar");
    Profil p("Budi");
    Profil p2 = p;                  // copy implisit: std::string yang bekerja
    Profil p3 = std::move(p);       // move implisit
    std::println("p2='{}' p3='{}'", p2.nama(), p3.nama());

    std::println("-- = delete / = default: copy dilarang, move tetap ada");
    Sertifikat c("SERT-001");
    Sertifikat c2 = std::move(c);   // sah: move default
    std::println("c2.kode()='{}'", c2.kode());
    return 0;
}

Output yang Dihasilkan:

text
-- Rule of Five: lima operasi dengan jejak
[ctor ] 'laporan' (alokasi buffer)
[copy ] 'laporan' (buffer baru, isi diduplikasi)
[move ] 'laporan' (buffer diambil alih, sumber kosong)
[copy= ] 'laporan' (resource lama dilepas, isi diduplikasi)
[ctor ] 'sementara' (alokasi buffer)
[move= ] 'sementara' (resource lama dilepas, buffer diambil alih)
m.data()='sementara' s.data()='laporan'
-- Rule of Zero: nol deklarasi fungsi khusus, perilaku tetap benar
p2='Budi' p3='Budi'
-- = delete / = default: copy dilarang, move tetap ada
c2.kode()='SERT-001'

Mari kita bedah:

  1. Baris 86-90 (lima operasi berurutan): konstruksi alokasi buffer, copy menduplikasinya, move mengambil alihnya, copy assignment melepas resource lama dulu sebelum menduplikasi, dan move assignment melepas resource lama sebelum mengambil alih buffer temporary — kelima jejak [ctor ], [copy ], [move ], [copy= ], [move= ] tercetak persis sesuai urutan.
  2. Baris 25-29 dan 42-51 (pasangan move): keduanya noexcept, menol-kan sumber sebelum keluar, dan menjaga guard this != &lain pada assignment — pola lengkap dari chapter Move Constructor dan Move Assignment.
  3. Baris 53 (destructor): delete[] data_ aman untuk pointer nullptr — syarat agar objek moved-from berakhir dengan rapi.

Rule of Zero: Delegasikan ke Member yang Mengelola Diri #

Class Profil pada baris 63-69 menunjukkan pilihan yang paling sering benar: resource disimpan sebagai std::string — member yang sudah punya kelima fungsi khusus yang benar — sehingga Profil sendiri tidak menulis satu pun. Copy, move, dan destructor implisitnya mengikuti anggota-anggotanya, dan semua baris jejak yang kita tulis manual untuk Tugas di atas sebenarnya sudah ditulis sekali saja oleh penulis pustaka standar. Pustaka standar menyediakan anggota pengelola untuk hampir semua jenis resource: std::string dan std::vector untuk data, std::unique_ptr untuk objek tunggal di heap, std::fstream untuk file, std::jthread untuk thread. Bila sebuah class hanya menggabungkan anggota-anggota semacam ini, Rule of Zero memberi hasil yang benar dengan nol baris kode yang bisa salah.

= default dan = delete: Menyatakan Niat Secara Eksplisit #

Class Sertifikat pada baris 72-82 memperlihatkan dua kata kunci yang melengkapi aturan di atas. = delete melarang copy — cocok untuk objek yang mewakili resource unik seperti sertifikat, handle, atau koneksi; compiler menolak program yang mencoba menyalinnya:

cpp menyalin objek yang copy-nya di-delete (tidak dikompilasi)
Sertifikat c("SERT-001");
Sertifikat c2 = c;   // copy constructor di-delete

Pesan error yang dihasilkan compiler:

text
ch12-copy.cpp: In function ‘int main()’:
ch12-copy.cpp:17:21: error: use of deleted function ‘Sertifikat::Sertifikat(const Sertifikat&)’
   17 |     Sertifikat c2 = c;   // gagal: copy constructor di-delete
      |                     ^
ch12-copy.cpp:7:5: note: declared here
    7 |     Sertifikat(const Sertifikat&) = delete;
      |     ^~~~~~~~~~
   17 |     Sertifikat c2 = c;   // gagal: copy constructor di-delete
      |                     ^

= default (baris 77-78) adalah kebalikannya: meminta compiler menghasilkan implementasi move default — persis perilaku implisit — sambil tetap menyatakan niat secara eksplisit di dalam kode. Kombinasi keduanya menghasilkan class move-only: tidak bisa disalin, tetapi bebas dipindahkan.

Penutup Section: Move Semantics dan Value Categories #

Sepanjang dua belas chapter, kita membangun pemahaman dari fondasi sampai atap: setiap ekspresi memiliki kategori (lvalue, prvalue, xvalue); rvalue reference T&& mengikat nilai sementara; move constructor dan move assignment mentransfer resource dengan biaya konstan asal ditandai noexcept; std::move hanyalah cast, std::forward memulihkan kategori asal lewat reference collapsing; dan compiler bisa melewati semuanya lewat elision yang dijamin untuk prvalue. Di atas fondasi itu, Rule of Zero memberi arah desain: tulis sedikit mungkin, dan delegasikan pengelolaan resource ke member yang mengelola dirinya sendiri.

Aturan-aturan ini bukan akhir jalan — mereka prasyarat untuk topik berikutnya. Di Section Concurrency dan Parallelism, kita akan membuat program berjalan di banyak thread sekaligus, dan pertanyaan pertama yang muncul adalah milik section ini: siapa pemilik data yang dikirim ke thread lain? Bagaimana menyerahkan objek ke thread tanpa salinan — dan tanpa dua thread berbagi satu buffer secara diam-diam? Pertanyaan itu bahkan sudah terjawab sebagian oleh satu fakta yang kini bisa kita baca dengan lancar: std::thread adalah class move-only — copy-nya di-delete, dan penyerahannya ke thread hanya lewat move. Sampai jumpa di sana.

Rujukan resmi: cppreference — rule of three/five/zero .