std::shared_ptr adalah smart pointer dengan kepemilikan bersama, yaitu pembungkus pointer yang mengizinkan banyak pemilik untuk satu objek dan menghancurkan objek itu hanya setelah pemilik terakhirnya pergi. Untuk mencapai tujuan tersebut, ia menyimpan penghitung pemilik di sebuah control block. Topik ini membahas cara kerja penghitung tersebut, peran std::weak_ptr sebagai pemegang yang tidak memiliki, serta jebakan siklus referensi yang hanya dapat diputus oleh weak_ptr.
Kepemilikan bersama dan control block#
Setiap std::shared_ptr membawa dua hal: pointer ke objeknya dan pointer ke control block-nya. Control block menyimpan dua penghitung, yaitu jumlah pemilik kuat yang mengelola objek dan jumlah pemegang lemah yang hanya mengamatinya. Penyalinan sebuah shared_ptr menambah penghitung kuat secara atomik, sedangkan penghancurannya menguranginya, dan objek yang dipegang dihancurkan tepat ketika penghitung kuat mencapai nol.
Program berikut mengamati penghitung itu lewat use_count():
#include <iostream>
#include <memory>
#include <string>
class Dokumen {
public:
explicit Dokumen(std::string nama) : nama_(nama) {
std::cout << " Dokumen " << nama_ << " dibangun\n";
}
~Dokumen() {
std::cout << " Dokumen " << nama_ << " dihancurkan\n";
}
std::string nama() const { return nama_; }
private:
std::string nama_;
};
int main() {
auto utama = std::make_shared<Dokumen>("laporan");
std::cout << "use_count awal: " << utama.use_count() << '\n';
{
auto salinan = utama;
std::cout << "use_count di dalam blok: " << utama.use_count() << '\n';
std::cout << "isi dari salinan: " << salinan->nama() << '\n';
}
std::cout << "use_count setelah blok: " << utama.use_count() << '\n';
utama.reset();
std::cout << "use_count setelah reset: " << utama.use_count() << '\n';
} Dokumen laporan dibangun
use_count awal: 1
use_count di dalam blok: 2
isi dari salinan: laporan
use_count setelah blok: 1
Dokumen laporan dihancurkan
use_count setelah reset: 0Urutannya menunjukkan masa hidup objek bergantung pada pemilik terakhir, bukan pada variabel pertama. Objek Dokumen baru dihancurkan setelah reset(), yaitu ketika penghitung kuat menyentuh nol. Pembuatan objek pada program di atas memakai std::make_shared, yang membuat objek dan control block-nya dalam satu alokasi sehingga lebih hemat daripada membangun shared_ptr dari new terpisah.
Kepemilikan bersama memang cocok untuk situasi tertentu. Contohnya adalah buffer data yang dibagikan ke beberapa pekerja tanpa diketahui pekerja mana yang selesai terakhir, graf antarobjek yang bagian-bagiannya benar-benar dimiliki bersama, atau objek yang dirujuk dari beberapa thread dan harus tetap hidup sampai semua pemakaiannya selesai. Di luar pola seperti itu, kepemilikan tunggal biasanya lebih tepat. Satu catatan penting menyertai pola ini: penghitung di control block diperbarui secara atomik sehingga operasi salinannya aman dijalankan dari banyak thread, tetapi perlindungan itu hanya mencakup penghitungnya, dan bukan objek yang dirujuknya. Akses baca dan tulis terhadap objek yang sama dari beberapa thread tetap membutuhkan mutex tersendiri.
weak_ptr: meminjam tanpa memiliki#
std::weak_ptr<T> menunjuk ke objek yang sama dengan shared_ptr tanpa menambah penghitung kuatnya, sehingga keberadaannya tidak pernah menunda penghancuran objek. Pemegang lemah ini hanya dapat diaktifkan lewat lock(), yang mengembalikan shared_ptr sementara bila objeknya masih hidup dan nullptr bila objeknya sudah dihancurkan. Fungsi expired() menanyakan hal yang sama tanpa mengembalikan shared_ptr apa pun.
Selain memutus siklus, pemegang lemah muncul di dua pola yang serupa. Cache menyimpan weak_ptr ke hasil perhitungannya, lalu memanggil lock() setiap kali hasil itu diminta, dan bila expired() menunjukkan objeknya sudah tidak ada, hasilnya dihitung ulang. Pola pengamat bekerja dengan cara yang sama: daftar pengamat berisi weak_ptr, sehingga pengamat yang objeknya sudah dihancurkan dapat dilepas dari daftar tanpa menyisakan rujukan menggantung. Kedua pola itu bertumpu pada satu sifat yang sama, yaitu keberadaan rujukan yang tidak memengaruhi masa hidup objeknya.
Siklus referensi dan pemutusannya#
Kepemilikan bersama menciptakan jebakan yang tidak dimiliki kepemilikan tunggal. Apabila dua objek saling menyimpan shared_ptr ke pasangannya, masing-masing penghitung tidak pernah mencapai nol, karena setiap objek selalu dimiliki pasangannya sendiri. Memori keduanya bocor tanpa jejak pada keluaran program, dan kebocoran itu hanya dapat ditemukan lewat alat pemeriksa memori. Karena itu, pencegahan lewat desain jauh lebih murah daripada pencarian sesudah kejadian. Versi bocornya tampak seperti berikut:
// tidak-dijalankan
class Undangan {
std::string nama_;
std::shared_ptr<Undangan> balasan_; // milik penuh atas pasangan
};
// ali->balasan_ = citra;
// citra->balasan_ = ali;
// Saat main berakhir, use_count kedua objek masih 1 karena saling menunjuk,
// sehingga kedua destructor tidak pernah berjalan dan kedua objek bocor.
Perbaikannya adalah mengubah salah satu arah rujukan menjadi weak_ptr, sehingga hubungan timbal baliknya tidak lagi saling menjaga. Pada program di bawah, Rekan menyimpan pasangannya sebagai weak_ptr, dan program mengamati penghitungnya sebelum memanggil lock().
Aturan desain yang menjaga pola ini tetap sederhana adalah mengarahkan kepemilikan ke satu arah saja. Objek induk memiliki anak-anaknya melalui unique_ptr atau shared_ptr, sedangkan rujukan balik dari anak ke induk dibuat sebagai weak_ptr atau referensi yang tidak memiliki. Dengan arah yang jelas begitu, siklus tidak terbentuk secara alami, dan pertanyaan siapa yang melepaskan apa selalu punya satu jawaban.
#include <iostream>
#include <memory>
#include <string>
class Rekan {
public:
explicit Rekan(std::string nama) : nama_(nama) {
std::cout << " Rekan " << nama_ << " dibangun\n";
}
~Rekan() {
std::cout << " Rekan " << nama_ << " dihancurkan\n";
}
void sambung(const std::weak_ptr<Rekan>& r) { pasangan_ = r; }
std::string pasangan() const {
if (auto r = pasangan_.lock()) {
return r->nama_;
}
return "(sudah pergi)";
}
private:
std::string nama_;
std::weak_ptr<Rekan> pasangan_;
};
int main() {
auto ali = std::make_shared<Rekan>("Ali");
auto citra = std::make_shared<Rekan>("Citra");
ali->sambung(citra);
citra->sambung(ali);
std::cout << "use_count ali: " << ali.use_count() << '\n';
std::cout << "use_count citra: " << citra.use_count() << '\n';
std::cout << "pasangan ali: " << ali->pasangan() << '\n';
std::cout << "akhir main\n";
} Rekan Ali dibangun
Rekan Citra dibangun
use_count ali: 1
use_count citra: 1
pasangan ali: Citra
akhir main
Rekan Citra dihancurkan
Rekan Ali dihancurkanKedua use_count bernilai satu karena rujukan silangnya tidak lagi dihitung sebagai kepemilikan. Ketika main berakhir, kedua shared_ptr lokal dihancurkan, penghitung kuat mencapai nol, dan kedua destructor berjalan dalam urutan terbalik dari pembangunannya.
Kesalahan umum#
- Memakai
std::shared_ptrsebagai pilihan default. Control block dan penghitung atomik bukan harga yang gratis, sedangkan sebagian besar relasi antarobjek memang berbentuk satu pemilik dan banyak peminjam. Pertimbangkanstd::unique_ptrdan referensi biasa terlebih dahulu, lalu pilihshared_ptrhanya bila kebutuhannya benar-benar kepemilikan bersama. - Memanggil
lock()lalu memakai hasilnya tanpa memeriksa.lock()mengembalikanshared_ptryang dapat kosong, dan dereference terhadapnya berarti dereferencenullptr. Pola amannya adalahif (auto r = w.lock())sebagaimana contohpasangan()di atas. - Membaca
use_count()sebagai alat kendali. Nilai itu hanya informasi diagnostik, dan di kode multithread nilainya sudah usang saat dicetak. Keputusan kepemilikan jangan pernah dibangun di atasnya. - Membuat siklus lewat panah yang tidak disadari. Siklus tidak selalu dua objek yang saling tunjuk langsung, tetapi bisa terbentuk lewat rantai panjang. Saat satu kelompok objek
shared_ptrtidak pernah dihancurkan walau tidak dipakai lagi, carilah rujukan balik di antara objek-objek tersebut dan ubah satu arahnya menjadiweak_ptr.
Ringkasan#
std::shared_ptrmembagi kepemilikan satu objek ke banyak pemilik, dan objeknya dihancurkan saat penghitung kuat di control block mencapai nol.std::make_sharedmembuat objek dan control block-nya dalam satu alokasi.std::weak_ptrmengamati objek tanpa menambah penghitung kuat, diaktifkan lewatlock(), dan dipakai untuk cache serta pemutusan siklus referensi.- Siklus
shared_ptrmembuat destructor tidak pernah berjalan, dan perbaikannya adalah mengubah satu arah rujukan menjadiweak_ptr.
Referensi#
- std::shared_ptr — cppreference.com
— Dokumentasi
shared_ptr, termasuk kontrakuse_count()dan perilaku salinannya. - std::weak_ptr — cppreference.com
— Dokumentasi
weak_ptr,lock(), danexpired(). - std::shared_ptr — LearnCpp — Pembahasan bertahap kepemilikan bersama dan jebakan siklusnya.