std::unique_ptr adalah smart pointer dengan kepemilikan eksklusif, yaitu pembungkus pointer yang menjamin hanya ada satu pemilik untuk satu objek dinamis. Ketika pemiliknya dihancurkan, objek yang dipegang ikut dihancurkan secara otomatis, sehingga setiap delete manual dapat dihilangkan dari kode. Topik ini membahas cara membuatnya dengan std::make_unique, cara mengalihkan kepemilikannya, dan cara menyesuaikan proses pelepasannya lewat deleter kustom.
Kepemilikan eksklusif dan make_unique#
Kontrak std::unique_ptr sederhana: satu objek, satu pemilik, dan pelepasan mengikuti masa hidup pemiliknya. Ketika sebuah fungsi memiliki std::unique_ptr<Kalkulasi>, objek Kalkulasi itu pasti dihancurkan tepat setelah fungsi berakhir, baik karena kembali normal maupun karena exception. Tidak ada jalur keluar yang dapat melewatkan pelepasan itu, karena pelepasan ditulis satu kali di destructor pembungkusnya sebagaimana pola RAII.
Sumber daya dibungkus sejak awal alokasinya. Fungsi std::make_unique<T>(...) mengalokasikan objek T dan langsung mengembalikan std::unique_ptr<T> yang memegangnya, sehingga tidak ada pointer mentah yang sempat lepas kendali di antara alokasi dan pembungkusannya. Fungsi ini tersedia sejak C++14 dan menjadi cara pembuatan yang disarankan.
Biaya pembungkus ini nyaris nol. std::unique_ptr<T> hanya menyimpan satu pointer, dan dengan deleter default tidak ada data tambahan yang tersisa setelah kompilasi, sehingga ukurannya sama dengan pointer mentah dan aksesnya sama cepatnya. Kepemilikan eksklusif juga membuat penalaran lebih mudah daripada pointer mentah. Setiap pembaca kode dapat melihat dari tipenya siapa pemilik objeknya, sedangkan pointer mentah tidak pernah menyatakan apakah pemegangnya wajib melepaskan objek itu atau hanya meminjamnya.
#include <iostream>
#include <memory>
#include <utility>
class Kalkulasi {
public:
explicit Kalkulasi(int n) : n_(n) {
std::cout << " Kalkulasi(" << n_ << ") dibangun\n";
}
~Kalkulasi() {
std::cout << " Kalkulasi(" << n_ << ") dihancurkan\n";
}
int nilai() const { return n_; }
private:
int n_;
};
void proses(std::unique_ptr<Kalkulasi> data) {
std::cout << " proses menerima kepemilikan: " << data->nilai() << '\n';
}
int main() {
auto p = std::make_unique<Kalkulasi>(42);
std::cout << "kirim ke proses dengan std::move:\n";
proses(std::move(p));
std::cout << "p kosong setelah alih? "
<< std::boolalpha << (p == nullptr) << '\n';
auto q = std::make_unique<Kalkulasi>(7);
std::cout << "lepaskan q lebih awal dengan reset:\n";
q.reset();
std::cout << "q kosong setelah reset? " << (q == nullptr) << '\n';
} Kalkulasi(42) dibangun
kirim ke proses dengan std::move:
proses menerima kepemilikan: 42
Kalkulasi(42) dihancurkan
p kosong setelah alih? true
Kalkulasi(7) dibangun
lepaskan q lebih awal dengan reset:
Kalkulasi(7) dihancurkan
q kosong setelah reset? trueKeluaran memperlihatkan urutan yang dijanjikan. Objek Kalkulasi(42) dihancurkan saat proses berakhir karena kepemilikan sudah berpindah ke parameternya, sedangkan q dihancurkan tepat di titik reset() dipanggil, jauh sebelum main berakhir. Baris p kosong setelah alih? true menegaskan bahwa pembungkus lama tidak lagi memegang apa pun setelah pengalihan.
Mengalihkan kepemilikan, bukan menyalinnya#
Copy constructor dan copy assignment std::unique_ptr dideklarasikan sebagai terhapus, sehingga upaya menyalinnya ditolak compiler. Satu-satunya cara memindahkan objek ke pemilik baru adalah move, yaitu pengalihan pointer dari pemilik lama ke pemilik baru.
// tidak-dijalankan
auto a = std::make_unique<Kalkulasi>(1);
std::unique_ptr<Kalkulasi> b = a; // error: copy constructor terhapus
std::unique_ptr<Kalkulasi> c = std::move(a); // sah: kepemilikan berpindah
Setelah pengalihan, a masih objek yang sah, tetapi ia tidak lagi memegang apa pun, dan pembandingan a == nullptr bernilai true sebagaimana terlihat pada keluaran program di bagian sebelumnya. Inilah alasan std::move wajib ditulis eksplisit: pengalihan kepemilikan adalah peristiwa yang berdampak, sehingga bahasa meminta penulisnya menyatakannya di tempat kejadian.
Tipe yang hanya bisa di-move disebut move-only type, dan sifat ini menular ke tempat objeknya disimpan. Sebuah std::vector<std::unique_ptr<Kalkulasi>> dapat tumbuh dan dipindahkan sebagai satu kesatuan, tetapi elemennya tidak pernah tersalin secara diam-diam, dan menambah elemennya membutuhkan push_back(std::make_unique<Kalkulasi>(n)). Ketika vector itu dihancurkan, seluruh objek yang ia pegang dihancurkan berurutan oleh destruktornya masing-masing. Pola inilah yang menggantikan vector pointer mentah dari era C++03 tanpa kehilangan polimorfisme, karena std::unique_ptr<KelasDasar> tetap menampung objek kelas turunan dan melepaskannya dengan destructor virtual.
Deleter kustom#
Secara default, std::unique_ptr<T> melepaskan objeknya dengan delete. Banyak sumber daya lama tidak dipakai dengan delete, misalnya std::FILE* yang wajib ditutup dengan std::fclose atau penangan dari pustaka C yang wajib dilepaskan dengan fungsinya sendiri. Untuk kasus ini, tipe deleter kustom diberikan sebagai parameter template kedua. Deleter dapat berupa lambda, dan tipe lambda tersebut dituliskan lewat decltype.
#include <cstdio>
#include <iostream>
#include <memory>
#include <string>
class KoneksiSederhana {
public:
explicit KoneksiSederhana(std::string alamat) : alamat_(alamat) {
std::cout << " terhubung ke " << alamat_ << '\n';
}
std::string alamat() const { return alamat_; }
private:
std::string alamat_;
};
int main() {
auto tutupDenganCatatan = [](KoneksiSederhana* k) {
std::cout << " deleter kustom menutup koneksi " << k->alamat() << '\n';
delete k;
};
std::unique_ptr<KoneksiSederhana, decltype(tutupDenganCatatan)>
koneksi(new KoneksiSederhana("sensor-lokal"), tutupDenganCatatan);
std::cout << " memakai koneksi: " << koneksi->alamat() << '\n';
std::cout << "akhir main\n";
} terhubung ke sensor-lokal
memakai koneksi: sensor-lokal
akhir main
deleter kustom menutup koneksi sensor-lokalBaris terakhir keluaran berasal dari deleter, bukan dari destructor KoneksiSederhana, dan berjalan saat main berakhir. Perhatikan bahwa deleter kustom wajib diberikan sebagai argumen constructor karena tipe lambda tidak memiliki constructor default pada C++17. Dengan pola ini, penangan sumber daya pustaka C apa pun dapat dibungkus menjadi objek RAII tanpa menulis kelas pembungkus baru. Pemakaian yang lebih umum dengan std::FILE* mengganti panggilan delete menjadi std::fclose di badan lambdanya.
Tipe deleter menjadi bagian dari identitas tipe pembungkusnya, sehingga std::unique_ptr<T, D1> dan std::unique_ptr<T, D2> adalah dua tipe yang berbeda meskipun T-nya sama. Konsekuensinya jelas: kode yang menyimpan atau menerima pembungkus tersebut wajib mengetahui cara pelepasannya. Lambda tanpa tangkapan berukuran kosong, dan compiler memadatkan penyimpanannya sehingga ukuran pembungkusnya tetap sebesar satu pointer. Lambda dengan tangkapan atau pointer fungsi menambah ukuran penanda keadaan deleter tersebut.
Kesalahan umum#
- Menyalin pointer mentah dari
get()lalu memakainya setelah pemiliknya tamat.get()mengembalikan pointer tanpa kepemilikan, dan pointer itu menggantung begitustd::unique_ptrdihancurkan atau di-reset. Gunakan pointer tersebut hanya untuk pemanggilan singkat, atau simpan salinan nilainya. - Membaca objek setelah kepemilikannya dialihkan.
std::unique_ptryang nilainya sudah dipindahkan adalah pointer kosong yang sah, sedangkan*pataup->padanya berarti dereferencenullptr. Periksa kekosongan bila jalur kodenya tidak jelas. - Membungkus satu pointer mentah dengan dua
std::unique_ptr. Kedua pembungkus merasa menjadi pemilik tunggal, lalu keduanya menghancurkan objek yang sama dan program gagal dengan double free. Satu objek dinamis hanya boleh dibungkus satu kali. - Membiarkan sumber daya besar hidup sampai akhir fungsi. Masa hidup pembungkusnya sama dengan masa hidup lingkupnya, sehingga objek besar yang hanya dipakai sepuluh baris pertama tetap menempati memori sampai fungsi berakhir. Panggil
reset()segera setelah pemakaiannya selesai agar pelepasan tidak ditunda tanpa alasan.
Ringkasan#
std::unique_ptrmenjamin satu objek dinamis hanya punya satu pemilik, dan objeknya dihancurkan otomatis saat pemiliknya tamat.std::make_uniquemenyatukan alokasi dan pembungkusan, tersedia sejak C++14, dan menjadi cara pembuatan yang disarankan.- Penyalinan dilarang, sedangkan pengalihan kepemilikan dilakukan lewat
std::movedan menyisakan pembungkus kosong di pemilik lama. - Deleter kustom sebagai parameter template kedua mengubah pelepasan menjadi panggilan apa pun, misalnya
std::fclose, tanpa kelas tambahan.
Referensi#
- std::unique_ptr — cppreference.com
— Dokumentasi lengkap
unique_ptr, termasuk aturan kepemilikan tunggal dan deleter kustomnya. - std::make_unique — cppreference.com
— Kontrak
make_uniquedan catatan ketersediaannya sejak C++14. - std::unique_ptr — LearnCpp
— Pembahasan bertahap
unique_ptrbeserta contoh pemakaiannya.