unique_ptr

Kepemilikan eksklusif, make_unique, dan deleter kustom.

Daftar isi

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.

cpp kepemilikan-tunggal.cpp
#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';
}
text Keluaran
  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? true

Keluaran 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.

cpp
// 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.

cpp deleter-kustom.cpp
#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";
}
text Keluaran
  terhubung ke sensor-lokal
  memakai koneksi: sensor-lokal
akhir main
  deleter kustom menutup koneksi sensor-lokal

Baris 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 begitu std::unique_ptr dihancurkan atau di-reset. Gunakan pointer tersebut hanya untuk pemanggilan singkat, atau simpan salinan nilainya.
  • Membaca objek setelah kepemilikannya dialihkan. std::unique_ptr yang nilainya sudah dipindahkan adalah pointer kosong yang sah, sedangkan *p atau p-> padanya berarti dereference nullptr. 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_ptr menjamin satu objek dinamis hanya punya satu pemilik, dan objeknya dihancurkan otomatis saat pemiliknya tamat.
  • std::make_unique menyatukan alokasi dan pembungkusan, tersedia sejak C++14, dan menjadi cara pembuatan yang disarankan.
  • Penyalinan dilarang, sedangkan pengalihan kepemilikan dilakukan lewat std::move dan 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#