std::shared_ptr menyelesaikan kepemilikan bersama, tetapi mekanisme hitungnya membawa lubang kecil yang klasik: bila dua objek saling memegang shared_ptr satu sama lain, hitungan mereka tidak akan pernah mencapai nol — keduanya menunggu yang lain dilepas lebih dulu, dan keduanya hidup sampai program berakhir. Kebocoran ala reference counting ini disebut circular reference. Solusi pustaka standar adalah std::weak_ptr<T>: pointer yang mengamati objek milik shared_ptr tanpa ikut menghitung sebagai pemilik. Chapter ini memvisualisasikan simpul kebocorannya, lalu membuktikan bahwa mengganti satu sisi panah dengan weak_ptr memulihkan pelepasan objek secara utuh.

Circular Reference: Hitungan yang Tak Pernah Nol #

Bayangkan dua objek yang saling merujuk — misalnya node list berantai yang menunjuk node berikutnya sekaligus sebelumnya. Bila KEDUA arah dipakai shared_ptr, masing-masing objek menganggap dirinya masih dipakai oleh yang lain:

Pada pola kiri, use_count A minimal 1 karena dipegang B, dan use_count B minimal 1 karena dipegang A — pelepasan dari luar pun tak pernah cukup untuk mencapai nol. Pada pola kanan, panah mundur diberi weak_ptr: ia mengamati A tanpa menambah hitungan, sehingga hitungan A kembali bergantung pada pemilik sebenarnya saja. Dalam kode, versi bermasalahnya tampak seperti ini:

cpp Pola bermasalah — dua shared_ptr saling menunjuk
struct Anak;
struct Ayah {
    std::shared_ptr<Anak> anak;
};
struct Anak {
    std::shared_ptr<Ayah> ayah;   // harusnya std::weak_ptr<Ayah>
};
// a->ayah = b; b->anak = a;  ->  kedua use_count tak pernah 0

weak_ptr: Mengamati Tanpa Memiliki #

std::weak_ptr tidak bisa mendereferensikan objeknya secara langsung — memang seharusnya, karena objek itu bisa berakhir kapan saja tanpa izinnya. Aksesnya lewat lock(), yang mengembalikan shared_ptr sementara: berhasil bila objek masih hidup, kosong bila sudah dilepas. Untuk pemeriksaan murah, expired() melaporkan apakah objek sudah berakhir. Program berikut merakit dua simpul yang saling merujuk dengan pola yang aman — panah maju shared_ptr, panah mundur weak_ptr — lalu membongkarnya:

cpp
#include <iostream>
#include <memory>
#include <string>

class Simpul {
public:
    explicit Simpul(const std::string& nama) : nama_(nama) {
        std::cout << nama_ << " diciptakan\n";
    }
    ~Simpul() {
        std::cout << nama_ << " dibebaskan\n";
    }

    std::shared_ptr<Simpul> berikutnya;   // panah maju: memiliki
    std::weak_ptr<Simpul> sebelumnya;     // panah mundur: hanya mengamati

    const std::string& nama() const { return nama_; }
private:
    std::string nama_;
};

int main() {
    auto a = std::make_shared<Simpul>("A");
    auto b = std::make_shared<Simpul>("B");

    a->berikutnya = b;    // A memegang B (shared_ptr)
    b->sebelumnya = a;    // B mengamati A (weak_ptr — tidak menambah count)

    std::cout << "a.use_count = " << a.use_count() << "\n";
    std::cout << "b.use_count = " << b.use_count() << "\n";

    // Akses aman lewat lock(): menghasilkan shared_ptr sementara
    if (std::shared_ptr<Simpul> aman = b->sebelumnya.lock()) {
        std::cout << "lock() berhasil, mengamati: " << aman->nama() << "\n";
    }

    a.reset();   // A dibebaskan karena count-nya tidak dipegang weak_ptr
    std::cout << "b->sebelumnya expired? "
              << (b->sebelumnya.expired() ? "ya" : "tidak") << "\n";

    b.reset();   // B dibebaskan
    std::cout << "selesai\n";
    return 0;
}

Output yang Dihasilkan:

text
A diciptakan
B diciptakan
a.use_count = 1
b.use_count = 2
lock() berhasil, mengamati: A
A dibebaskan
b->sebelumnya expired? ya
B dibebaskan
selesai

Bagian-bagian pembuktian dari program di atas:

  1. Baris 26-27 (pengaitan dua arah): berikutnya bertipe shared_ptr menambah hitungan B, sedangkan sebelumnya bertipe weak_ptr tidak menambah hitungan A — terbukti dari a.use_count = 1.
  2. Baris 33 (lock()): b->sebelumnya.lock() menghasilkan shared_ptr sementara bernama aman; hitungan A naik selama aman hidup, dan A dijamin tidak bisa berakhir di tengah pemakaiannya.
  3. Baris 37 (a.reset()): karena weak_ptr tidak ikut menghitung, hitungan A menyentuh nol dan A dibebaskan tercetak — inilah yang takkan pernah terjadi pada pola circular.
  4. Baris 38-39 (expired()): weak_ptr yang tersisa jujur melaporkan kondisi objeknya: expired? ya. Pembanding if (auto s = w.lock()) menjadi cara idiomatik membaca keadaan ini.
  5. Baris 41 (b.reset()): panah milik A sudah ikut terlepas bersama A, sehingga hitungan B kini tinggal satu — dan B pun dilepas.

Kapan weak_ptr Layak Dipakai #

  • Back-reference pada struktur berantai: anak menunjuk induk, mundur menunjuk maju — arah yang tidak harus mempertahankan hidup target.
  • Cache: cache menyimpan weak_ptr agar objek tidak dipertahankan hidup hanya karena tercatat; bila pemilik sebenarnya sudah pergi, cache laporkan kosong.
  • Observer: banyak pengamat melihat satu subjek tanpa mencegah subjek berakhir; terlihat pada Section Concurrency dan Parallelism nanti dalam bentuk serupa.

Dengan weak_ptr, tiga smart pointer pustaka standar sudah lengkap: pemilik tunggal, pemilik bersama, dan pengamat. Chapter berikutnya menyusun ketiganya — ditambah pointer mentah — menjadi satu peta kepemilikan yang menentukan alat mana untuk kebutuhan mana.

Referensi: cppreference — std::weak_ptr , cppreference — weak_ptr::lock , cppreference — weak_ptr::expired .