API yang kita desain di chapter sebelumnya masih menyisakan permukaan yang jarang disadari: setiap class membawa set fungsi khusus — copy constructor, copy assignment, move constructor, move assignment, dan destructor — yang dipanggil compiler tanpa satu baris pun ditulis pemakainya. Apakah Laporan bisa disalin ke dalam std::vector, apakah BerkasTeks bisa dipindahkan antar function, dan apa yang terjadi saat object itu berakhir — semuanya keputusan API yang sama pentingnya dengan nama dan parameter. Kebijakan desain untuk keputusan itu dirangkum dalam satu istilah: Rule of Zero — tulis nol fungsi khusus dan delegasikan pengelolaan resource ke member yang mengelola dirinya sendiri. Chapter ini menjadikannya pilihan default yang disengaja, menunjukkan satu-satunya kondisi yang memaksa kita menulis kelima fungsi sekaligus, dan menutup persiapan desain section ini.

Fungsi Khusus adalah API yang Tak Terlihat #

Recap singkat dari perjalanan panjang sebelumnya: Section Modern C++ memperkenalkan move semantics secara pragmatis — copy menduplikasi isi, move mengambil alih resource dengan biaya konstan, dan std::move hanyalah cast yang meminta dipindahkan. Section Move Semantics dan Value Categories kemudian membongkar mesinnya sampai ke rvalue reference dan copy elision, ditutup chapter Rule of Zero, Three, Five yang merangkum mekanikanya. Yang belum kita lakukan adalah membacanya sebagai keputusan desain dalam konteks section ini: kelima fungsi khusus menentukan apa yang boleh dilakukan pemakai terhadap class kita — std::vector<Laporan> menuntut Laporan bisa dipindah, parameter by value menyalin atau memindah, dan destructor menjalankan pembersihan di setiap scope tanpa diminta.

Karena dipanggil secara implisit, fungsi khusus adalah kontrak yang paling sering dilupakan saat review: tak ada yang menulis laporan.copy() di kode pemakai, tetapi salinannya terjadi di baris yang kelihatan sepele. Kebijakannya perlu diputuskan secara sadar, bukan diserahkan pada kebetulan generator implisit compiler.

Satu Program, Dua Kebijakan #

Program berikut menempatkan dua kebijakan berdampingan: Laporan yang menganut Rule of Zero dengan member yang mengelola dirinya, dan BerkasTeks yang mengelola handle C mentah FILE* sehingga wajib menulis kelima fungsi khusus:

cpp
// sumber-daya.cpp — Rule of Zero sebagai default, Rule of Five hanya untuk resource mentah
// Kompilasi: g++ -std=c++23 -Wall -Wextra sumber-daya.cpp -o sumber-daya

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

// ===== Rule of Zero: semua member mengelola dirinya, nol fungsi khusus ditulis =====
class Laporan {
public:
    void tambahBaris(std::string teks) {
        baris_.push_back(std::move(teks));
    }

    int totalBaris() const {
        return static_cast<int>(baris_.size());
    }

private:
    std::vector<std::string> baris_;   // copy, move, destructor: benar otomatis
};

// ===== Rule of Five: class mengelola handle C mentah (FILE*), lima operasi dinyatakan =====
class BerkasTeks {
public:
    BerkasTeks(const std::string& path, const std::string& mode)
        : path_(path) {
        handle_ = std::fopen(path.c_str(), mode.c_str());
        std::println("[buka ] '{}' {}", path, handle_ != nullptr ? "berhasil" : "GAGAL");
    }

    ~BerkasTeks() {
        if (handle_ != nullptr) {
            std::fclose(handle_);
            std::println("[tutup] '{}' ditutup oleh destructor", path_);
        }
    }

    // resource unik: satu handle, satu pemilik — copy dilarang lewat = delete
    BerkasTeks(const BerkasTeks&) = delete;
    BerkasTeks& operator=(const BerkasTeks&) = delete;

    // move ditulis manual: alihkan handle, lalu kosongkan sumber
    BerkasTeks(BerkasTeks&& lain) noexcept
        : path_(std::move(lain.path_)), handle_(lain.handle_) {
        lain.handle_ = nullptr;
        std::println("[pindah] handle '{}' berpindah pemilik", path_);
    }

    BerkasTeks& operator=(BerkasTeks&& lain) noexcept {
        if (this != &lain) {
            if (handle_ != nullptr) {
                std::fclose(handle_);          // handle lama dilepas dulu
                std::println("[tutup] handle lama dilepas oleh move assignment");
            }
            path_ = std::move(lain.path_);
            handle_ = lain.handle_;
            lain.handle_ = nullptr;
            std::println("[pindah=] handle '{}' berpindah pemilik", path_);
        }
        return *this;
    }

    void tulis(const std::string& teks) {
        if (handle_ != nullptr) {
            std::fputs((teks + "\n").c_str(), handle_);
        }
    }

    // API yang jujur: baris berikutnya, atau std::nullopt saat habis
    std::optional<std::string> bacaBaris() {
        char teks[128];
        if (handle_ != nullptr && std::fgets(teks, sizeof teks, handle_) != nullptr) {
            std::string hasil = teks;
            if (!hasil.empty() && hasil.back() == '\n') {
                hasil.pop_back();
            }
            return hasil;
        }
        return std::nullopt;
    }

    bool terbuka() const { return handle_ != nullptr; }

private:
    std::string path_;
    std::FILE* handle_ = nullptr;
};

int main() {
    const std::string path = "sumber-daya-demo.txt";

    std::println("-- Rule of Zero: Laporan tidak menulis satu pun fungsi khusus --");
    Laporan a;
    a.tambahBaris("kopi");
    a.tambahBaris("teh");
    Laporan b = a;                  // copy implisit: salinan penuh, dua object berdiri sendiri
    b.tambahBaris("gula");
    std::println("a={} baris, b={} baris (copy tidak saling mengganggu)",
                 a.totalBaris(), b.totalBaris());
    Laporan c = std::move(b);       // move implisit: isi berpindah, tanpa duplikasi
    std::println("c={} baris (hasil move)", c.totalBaris());

    std::println("-- Rule of Five: BerkasTeks mengelola FILE* mentah --");
    {
        BerkasTeks keluar(path, "w");
        keluar.tulis("pesan pertama");
        BerkasTeks pindahan = std::move(keluar);   // handle berpindah, bukan disalin
        pindahan.tulis("pesan kedua (oleh pemilik baru)");
    }                               // tutup scope: TEPAT SATU fclose dari destructor

    std::println("-- verifikasi isi berkas --");
    {
        BerkasTeks masuk(path, "r");
        while (auto baris = masuk.bacaBaris()) {
            std::println("terbaca: {}", *baris);
        }
    }

    std::remove(path.c_str());      // bersih-bersih demo
    std::println("berkas demo dihapus");
    return 0;
}

Output yang Dihasilkan:

text
-- Rule of Zero: Laporan tidak menulis satu pun fungsi khusus --
a=2 baris, b=3 baris (copy tidak saling mengganggu)
c=3 baris (hasil move)
-- Rule of Five: BerkasTeks mengelola FILE* mentah --
[buka ] 'sumber-daya-demo.txt' berhasil
[pindah] handle 'sumber-daya-demo.txt' berpindah pemilik
[tutup] 'sumber-daya-demo.txt' ditutup oleh destructor
-- verifikasi isi berkas --
[buka ] 'sumber-daya-demo.txt' berhasil
terbaca: pesan pertama
terbaca: pesan kedua (oleh pemilik baru)
[tutup] 'sumber-daya-demo.txt' ditutup oleh destructor
berkas demo dihapus

Mari kita bedah keputusan desainnya:

  1. Baris 12-24 (Laporan): tidak ada satu pun dari lima fungsi khusus yang ditulis — satu-satunya member adalah std::vector<std::string>, dan vector itu sudah punya kelima implementasi yang benar, ditulis sekali oleh penulis pustaka standar.
  2. Baris 14 (tambahBaris(std::string teks)): parameter by value lalu std::move — pola sink yang menyimpan nilai langsung ke rumah barunya tanpa salinan ekstra.
  3. Baris 100-102 (copy implisit): Laporan b = a menghasilkan salinan penuh; baris b=3 berbanding a=2 membuktikan keduanya object terpisah yang tidak saling mengganggu.
  4. Baris 104 (move implisit): Laporan c = std::move(b) memindahkan isi vector tanpa menduplikasi satu string pun — jalur cepat yang otomatis tersedia karena kita tidak menulis apa pun.
  5. Baris 27-38 (BerkasTeks): constructor membuka FILE* dan destructor menutupnya — pola RAII dari chapter RAII pada resource yang tidak punya pengelola pustaka; object yang hidup di stack menjamin handle tertutup di setiap jalur keluar.
  6. Baris 43-44 (= delete): copy dinyatakan tidak ada — satu handle berarti satu pemilik, dan compiler menolak setiap usaha menyalinnya, seperti segera kita buktikan.
  7. Baris 47-51 dan 53-64 (move manual): move constructor mengalihkan pointer lalu mengosongkan sumber; move assignment menambahkan guard this != &lain dan melepas handle lama sebelum mengambil alih yang baru. Keduanya noexcept — syarat agar container berani memakai jalur pindah.
  8. Output baris ke-6 dan ke-11: tepat satu [tutup] per handle — bukti eksekusi bahwa tidak ada fclose ganda, walau keluar sudah dilewati std::move dan dua object BerkasTeks sempat hidup berdampingan.
  9. Baris 74 (std::optional<std::string> bacaBaris()): API class ini juga mengikuti kaidah chapter API Design — “tidak ada baris lagi” disampaikan lewat std::nullopt, bukan sentinel teks kosong yang ambigu.

= delete dan = default: Menyatakan Niat #

= delete pada copy BerkasTeks bukan dekorasi — ia memindahkan larangan dari dokumentasi ke compiler. Uji kompilasi kecil membuktikannya:

cpp menyalin object yang copy-nya di-delete (tidak dikompilasi)
Kunci a("K-01");
Kunci b = a;   // gagal: copy constructor di-delete

Pesan error yang dihasilkan compiler:

text
uji-copy.cpp: In function ‘int main()’:
uji-copy.cpp:16:15: error: use of deleted function ‘Kunci::Kunci(const Kunci&)’
   16 |     Kunci b = a;   // gagal: copy constructor di-delete
      |               ^
uji-copy.cpp:8:5: note: declared here
    8 |     Kunci(const Kunci&) = delete;             // copy dilarang
      |     ^~~~~

Pesan di atas berasal dari uji kompilasi kecil di luar chapter — alamat berkas dan nomor barisnya sesuai berkas uji tersebut, tetapi bagian yang penting selalu sama: use of deleted function. Pemakai yang mencoba menyalin object move-only tertangkap saat kompilasi, jauh sebelum program dijalankan.

= default adalah kembarannya yang sering disalahpahami: ia bukan “menulis ulang” fungsi khusus, melainkan meminta compiler menghasilkan implementasi default sambil menyatakan niat itu di dalam kode. Kegunaan paling praktisnya adalah memulihkan move yang hilang. Menulis satu fungsi khusus saja — biasanya destructor — membuat compiler berhenti menghasilkan move implisit, dan class tiba-tiba kehilangan jalur cepatnya tanpa satu pesan error:

cpp destructor menulis melumpuhkan move implisit (sketsa)
class Cache {
public:
    ~Cache() { /* ... */ }     // destructor ditulis...
    // ...dan move constructor + move assignment IMPLISIT HILANG.
    // Copy masih dibuat (status usang) -> vector jatuh ke jalur salinan.
};

class CacheBaik {
public:
    ~CacheBaik() { /* ... */ }
    CacheBaik(CacheBaik&&) noexcept = default;         // move dipulihkan
    CacheBaik& operator=(CacheBaik&&) noexcept = default;
    CacheBaik(const CacheBaik&) = delete;
    CacheBaik& operator=(const CacheBaik&) = delete;
};

Perilaku ini terverifikasi langsung: std::vector<Cache>::push_back pada class pertama benar-benar jatuh ke copy constructor, sedangkan versi CacheBaik yang sama-sama punya destructor tidak menyalin sama sekali karena move-nya di-default. Perhatikan juga satu hal di BerkasTeks: move-nya ditulis manual, bukan = default.

Peta Keputusan: Zero, Five, atau Non-Copyable #

Dengan dua kebijakan dan dua kata kunci di atas, keputusan untuk setiap class bisa diringkas:

Situasi classKebijakan yang tepat
Semua member mengelola dirinya (std::string, std::vector, std::unique_ptr, std::fstream, std::jthread)Rule of Zero — nol deklarasi fungsi khusus
Mengelola resource mentah yang tak punya pengelola pustaka (FILE*, handle C library, socket)Rule of Five penuh — kelima fungsi ditulis sekaligus
Resource unik satu pemilik yang diatur sendiri= delete untuk copy, move manual atau = default
Object yang tidak boleh disalin maupun dipindah= delete untuk copy dan move

Rule of Zero menang sebagai default bukan karena lebih pendek, melainkan karena memindahkan kebenaran ke tempat yang sudah teruji: member pengelola seperti std::unique_ptr menuliskan kelima fungsi itu sekali, dengan test yang menyertainya, untuk semua class yang memakainya. Menulis Rule of Five adalah pengecualian yang jujur — kasus BerkasTeks — dan pengecualian itu justru lebih aman karena keempat class lain di sekitarnya tidak ikut menulis kode pengelolaan yang rawan.

Semua batu bata desain section ini kini lengkap: modul yang terpisah, antarmuka yang dirancang, resource yang dikelola dengan aturan yang benar. Namun kode yang baik pun tetap akan ditimpa kebutuhan baru — dan mengubah struktur kode yang sudah berjalan tanpa merusak perilakunya adalah keterampilan tersendiri. Topik persis itulah yang menutup section ini: refactoring.

Referensi: Rule of three/five/zero (cppreference) , std::unique_ptr (cppreference) .