Pratinjau smart pointer

Bagaimana C++ modern menutup masalah memori manual sebelum pembahasan penuh.

Daftar isi

Smart pointer adalah objek yang membungkus pointer mentah dan melepas memori yang dikelolanya secara otomatis pada akhir masa hidupnya. Pemindahan tanggung jawab ini menutup kebocoran dan pelepasan ganda yang selama ini harus dicegah secara manual. Topik ini memperkenalkan std::unique_ptr dan std::make_unique sebagai gambaran awal, sedangkan pembahasan penuhnya, termasuk shared_ptr dan move semantics, berada pada modul RAII, smart pointer, dan move semantics.

unique_ptr: pemilik tunggal#

std::unique_ptr<T> adalah smart pointer yang menjadi satu-satunya pemilik objek dinamisnya. Saat masa hidup unique_ptr berakhir, misalnya saat fungsi yang mendeklarasikannya kembali, delete dijalankan secara otomatis tanpa perlu ditulis. Kepemilikannya tunggal, sehingga unique_ptr tidak dapat disalin, dan salinan ganda yang menyebabkan pelepasan ganda tidak mungkin terjadi.

Harga yang dibayar adalah aturan pemakaiannya yang ketat, yaitu objek yang dibuat tetap milik satu unique_ptr sampai kepemilikannya dipindahkan. Aturan ini tampak membatasi pada awalnya, tetapi bentuk satu pemilik justru yang paling sering dibutuhkan, karena sebagian besar objek dinamis memang hanya dimiliki satu tempat. Tipe lengkapnya adalah std::unique_ptr<T> dari header <memory>, dan program berikut menggunakannya untuk mengelola objek kelas sederhana.

Penulisan tipenya tidak memakai tanda bintang pada deklarasi. std::unique_ptr<int> p mendeklarasikan pemilik objek int, dan aksesnya tetap memakai *p sebagaimana pointer biasa. Perbedaan hanya ada pada aturan penyalinannya, bukan pada cara membaca atau menulis nilai yang ditunjuk.

Biaya pemakaiannya nyaris tidak ada. Sebuah unique_ptr tanpa deleter kustom, yaitu fungsi pelepasan pilihan sendiri, berukuran sama dengan pointer mentah, dan kode pelepasan otomatisnya adalah kode yang sama dengan yang wajib ditulis manual sebelumnya. Compiler yang mengoptimasi umumnya menghilangkan seluruh lapisan pembungkusnya, sehingga keamanan ini diperoleh tanpa perbedaan kecepatan yang terukur pada kebanyakan program.

cpp unique-ptr-dasar.cpp
#include <iostream>
#include <memory>
#include <string>

class Catatan {
public:
    explicit Catatan(std::string isi) : isi_{std::move(isi)} {
        std::cout << "Catatan dibuat: " << isi_ << '\n';
    }
    ~Catatan() {
        std::cout << "Catatan dihapus: " << isi_ << '\n';
    }
    const std::string& isi() const { return isi_; }

private:
    std::string isi_;
};

int main() {
    auto catatan = std::make_unique<Catatan>("rapat pukul 10");

    std::cout << "Isi: " << catatan->isi() << '\n';
    return 0;
}
text Keluaran
Catatan dibuat: rapat pukul 10
Isi: rapat pukul 10
Catatan dihapus: rapat pukul 10

Keluaran di atas memperlihatkan urutan peristiwa yang menjadi inti smart pointer. Constructor berjalan saat objek dibuat, lalu destructor berjalan otomatis saat main berakhir, tanpa satu baris delete pun ditulis. Constructor dan destructor dibahas lengkap pada modul OOP bagian I, tetapi urutannya sudah terlihat jelas di sini. Pada fungsi dengan banyak jalur keluar, perilaku ini sama-sama berlaku, sehingga tidak ada jalur yang membocorkan memori.

make_unique dan pola pembuatan#

std::make_unique<T>(...) membuat objek T dan langsung membungkusnya pada unique_ptr dalam satu ekspresi. Bentuk ini disarankan daripada menjalankan new lalu membungkus hasilnya secara terpisah, karena tidak ada jeda saat alamat mentah berjalan sendirian. Sejak C++14, make_unique tersedia pada pustaka standar, dan kebutuhan argumennya mengikuti constructor T.

Polanya sederhana, yaitu auto p = std::make_unique<T>(argumen);. Kata kunci auto menghindari penulisan tipe yang panjang dua kali, dan hasilnya adalah pointer yang sudah aman sejak baris pertamanya. Tidak ada ekspresi new yang tampak pada kode pemanggil, sehingga titik alokasi terpusat dan mudah diaudit. Akses ke objeknya menggunakan operator * atau -> sebagaimana pointer biasa, sehingga kode yang sudah ada tidak perlu diubah bentuknya.

Alasan lain yang lebih teknis adalah kegagalan di tengah ekspresi. Apabila satu pemanggilan fungsi membuat dua objek melalui new yang terpisah, exception pada pembuatan kedua dapat menyisakan objek pertama tanpa pemilik. make_unique mempersempit jeda tersebut sehingga setiap objek yang sudah dibuat langsung terikat pada pembungkusnya.

Kepemilikan yang berpindah#

Objek unique_ptr tidak dapat disalin, tetapi kepemilikannya dapat dipindahkan dengan std::move. Setelah pemindahan, unique_ptr asal menjadi kosong dan berisi nullptr, sedangkan penerima menjadi satu-satunya pemilik objeknya. Perilaku ini menjaga invarian satu-pemilik tetap terjaga, sekaligus menyediakan cara sah untuk mengirim objek dinamis antar fungsi.

Program berikut memindahkan kepemilikan sebuah unique_ptr dan memeriksa keadaan kedua sisi sebelum dan sesudahnya.

cpp kepemilikan-berpindah.cpp
#include <iostream>
#include <memory>

int main() {
    auto p1 = std::make_unique<int>(45);

    std::cout << "p1 valid? " << std::boolalpha << (p1 != nullptr) << '\n';

    auto p2 = std::move(p1);
    std::cout << "p2 valid? " << (p2 != nullptr) << '\n';
    std::cout << "p1 valid? " << (p1 != nullptr) << '\n';
    std::cout << "Nilai lewat p2: " << *p2 << '\n';
    return 0;
}
text Keluaran
p1 valid? true
p2 valid? true
p1 valid? false
Nilai lewat p2: 45

Setelah std::move, p1 menjadi kosong sehingga pemeriksaannya menghasilkan false, sedangkan p2 memegang objek dengan nilai 45. Mengakses p1 setelah perpindahan tanpa pemeriksaan termasuk undefined behavior, sehingga kebiasaan memeriksa null yang dibangun pada topik nullptr tetap relevan di sini. Perbedaannya, pemeriksaan yang sama kini menjadi langkah yang jarang diperlukan, karena pemilik objek selalu jelas dan pelepasannya terjadwal otomatis.

Pola yang paling bermanfaat adalah fungsi yang mengembalikan unique_ptr. Fungsi pembuat objek dapat mengalokasikan, mengisi, lalu mengembalikan hasilnya sebagai pemilik tunggal, dan penerima menyimpannya pada unique_ptr miliknya sendiri. Rantai kepemilikan seperti ini menggantikan pola lama yang mengembalikan pointer mentah beserta catatan kaki tentang pihak yang wajib melepaskannya.

Kasus khusus tetap tersedia di pustaka standar. Untuk array dinamis, std::unique_ptr<T[]> memanggil delete[] secara otomatis, dan std::make_unique<T[]>(n) membuatnya dalam satu ekspresi. Untuk sumber daya selain memori, misalnya pegangan berkas (file handle), unique_ptr menerima deleter kustom yang pembahasannya berada pada modul RAII, smart pointer, dan move semantics.

Pratinjau ini sengaja dibatasi pada unique_ptr, karena pembahasan penuhnya mencakup shared_ptr, aturan pemilik bersama, dan hubungannya dengan move semantics. Meskipun begitu, pola dasarnya sudah utuh, yaitu alokasi yang dibungkus sejak awal dan pelepasan yang dijalankan oleh objek pembungkusnya. Kode yang mengikuti pola ini meniadakan kebocoran yang bersumber dari jalur eksekusi yang melompati delete, sebagaimana ditelusuri pada topik bug memori.

Ringkasan#

  • std::unique_ptr menjadi satu-satunya pemilik objek dinamisnya dan menjalankan delete otomatis pada akhir masa hidupnya.
  • std::make_unique membuat objek dan membungkusnya dalam satu ekspresi, sehingga tidak ada alamat mentah yang berjalan sendirian.
  • Kepemilikan berpindah dengan std::move, sedangkan penyalinan tidak tersedia, sehingga pelepasan ganda tidak mungkin terjadi.
  • Pembahasan penuh smart pointer, termasuk shared_ptr, berada pada modul RAII, smart pointer, dan move semantics.

Referensi#