Prinsip RAII (Resource Acquisition Is Initialization) menautkan setiap sumber daya kepada masa hidup sebuah objek: constructor memperoleh sumber daya saat objek dibangun, sedangkan destructor melepaskannya saat objek dihancurkan. Mekanisme ini berjalan otomatis, baik ketika fungsi kembali secara normal maupun ketika stack unwinding terjadi karena exception. Topik ini membahas gagasan dasarnya, menunjukkannya pada sebuah kelas pembungkus berkas, dan memetakan pola yang sama ke pustaka standar C++.
Masalah yang dijawab RAII#
Fungsi yang mengelola sumber daya secara manual wajib melepaskannya di setiap jalur keluar. Jalur keluar bukan hanya pernyataan return di akhir fungsi, tetapi juga setiap return lebih awal, setiap throw, dan setiap pemanggilan yang tidak pernah kembali. Apabila satu jalur terlewat, sumber daya tidak pernah dilepaskan. Pada berkas, akibatnya adalah deskriptor berkas yang tetap terbuka, sedangkan pada memori dinamis, akibatnya adalah kebocoran.
Pola manual berikut memperlihatkan kerapuhannya:
// tidak-dijalankan
void catatSuhuManual() {
std::FILE* berkas = std::fopen("suhu.log", "w");
if (berkas == nullptr) return;
if (dataSensorRusak()) {
return; // berkas tetap terbuka
}
proses(berkas);
std::fclose(berkas); // tidak pernah tercapai dari return di atas
}Fungsi di atas melepaskan berkasnya hanya pada satu dari tiga jalur keluar. Menambah std::fclose sebelum setiap return memperbaiki masalah ini, tetapi pola seperti itu mudah rusak setiap kali fungsi dimodifikasi dan membuat setiap pengembang baru wajib mengingat semua jalurnya. Masalah ini tidak terbatas pada berkas. Setiap sumber daya selalu datang sebagai pasangan operasi yang wajib berpasangan: std::fopen dengan std::fclose, new dengan delete, kunci dengan buka kunci. Semakin banyak pasangan yang dikelola satu fungsi, semakin banyak jalur yang wajib ditutup, dan semakin mahal biaya pemeriksaannya.
RAII membalik pendekatnya: pelepasan tidak ditulis di setiap jalur keluar, melainkan ditulis satu kali di destructor. Jumlah kode yang wajib benar berkurang dari sebesar jumlah jalur keluar menjadi hanya sebesar jumlah kelas pembungkusnya. Bagian yang sudah benar pun tidak perlu disentuh lagi.
Konstruktor memperoleh, destructor melepaskan#
Kelas RAII memindahkan seluruh tanggung jawab pelepasan ke destructor, sehingga setiap objek yang mencapai akhir masa hidupnya otomatis mengembalikan sumber daya yang dipegangnya. Kelas BerkasLog berikut memperoleh berkas di constructor, menuliskannya lewat method catat(), dan menutup berkas di destructor. Programnya menguji dua jalur keluar yang berbeda, yaitu pengembalian normal dan pelemparan exception.
#include <cstdio>
#include <iostream>
#include <stdexcept>
#include <string>
class BerkasLog {
public:
explicit BerkasLog(const char* jalur) {
berkas_ = std::fopen(jalur, "w");
if (berkas_ == nullptr) {
throw std::runtime_error("berkas tidak dapat dibuka");
}
std::cout << " " << jalur << " dibuka\n";
}
~BerkasLog() {
if (berkas_ != nullptr) {
std::fclose(berkas_);
std::cout << " berkas log ditutup oleh destructor\n";
}
}
void catat(const std::string& baris) {
std::fputs(baris.c_str(), berkas_);
std::fputc('\n', berkas_);
}
private:
std::FILE* berkas_;
};
void catatSuhu() {
BerkasLog log("suhu.log");
log.catat("suhu = 24");
std::cout << " pencatatan selesai\n";
}
void prosesDataRusak() {
BerkasLog log("sensor.log");
log.catat("mulai");
throw std::runtime_error("data sensor rusak");
}
int main() {
std::cout << "jalur normal:\n";
catatSuhu();
std::cout << "jalur exception:\n";
try {
prosesDataRusak();
} catch (const std::exception& e) {
std::cout << " menangkap: " << e.what() << '\n';
}
std::cout << "akhir main\n";
}jalur normal:
suhu.log dibuka
pencatatan selesai
berkas log ditutup oleh destructor
jalur exception:
sensor.log dibuka
berkas log ditutup oleh destructor
menangkap: data sensor rusak
akhir mainPada jalur normal, destructor BerkasLog berjalan saat objek log mencapai akhir fungsi catatSuhu(). Pada jalur exception, proses yang sama tetap berjalan tanpa kode tambahan apa pun: stack unwinding memanggil destructor dari setiap objek lokal sebelum exception sampai ke blok catch. Pesan berkas log ditutup oleh destructor muncul sebelum menangkap, dan urutan ini dijamin oleh bahasa. Inilah perbedaan mendasarnya dengan pola manual, yang tidak memiliki titik tunggal tempat pelepasan berlangsung.
Dua sifat membuat mekanisme ini kokoh. Pertama, perolehan dan pelepasan berada di satu tempat yang saling berhadapan, yaitu constructor dan destructor kelas yang sama, sehingga pasangan operasinya sulit terpisah. Kedua, destructor dijamin terpanggil tepat ketika objek keluar dari lingkupnya, tanpa bergantung pada optimasi compiler maupun kerapian penulisan pemanggil. Bila sebuah fungsi memakai beberapa objek RAII, destruktornya berjalan berurutan dalam arah terbalik dari urutan pembangunannya, dan urutan tersebut juga tetap pada jalur exception.
RAII pada pustaka standar#
Pustaka standar C++ memakai pola ini secara luas, sehingga sebagian besar sumber daya umum sudah memiliki pembungkusnya. std::vector memperoleh memori dinamis di constructor dan melepaskannya di destructor, sedangkan std::string mengelola buffer teksnya dengan cara yang sama. std::fstream membuka berkas saat dibangun dan menutupnya saat dihancurkan. Pada kode multithread, std::lock_guard mengunci mutex di constructor dan membukanya di destructor, sehingga mutex tidak pernah tertinggal terkunci ketika blok keluar lewat exception.
Modul ini melanjutkan pola yang sama ke memori dinamis. Smart pointer adalah objek pembungkus pointer mentah yang melepaskan objek yang dipegangnya lewat destructor, dan std::unique_ptr merupakan wujudnya yang paling sering dipakai. Dua topik berikutnya membahas smart pointer tersebut satu per satu.
Susunan kelas semacam ini juga menjelaskan istilah keamanan exception yang muncul pada modul penanganan error. Fungsi yang memakai objek RAII saja tidak menulis pernyataan pembersihan apa pun, sehingga jalur exception-nya tidak mungkin terlewat pelepasan. Kebenaran pembersihan berpindah dari disiplin penulisan fungsi menjadi disiplin desain kelas, yang ditulis sekali dan diuji sekali.
Kesalahan umum#
- Objek RAII disalin tanpa perlindungan. Compiler menghasilkan copy constructor yang menyalin pointer mentahnya, sehingga dua objek menutup berkas yang sama dan program dapat gagal saat kedua destructor berjalan. Perbaikannya adalah melarang penyalinan, menulis copy constructor yang menyalin sumber dayanya, atau mendelegasikan kepemilikan ke
std::unique_ptr. - Melepaskan sumber daya secara manual meski pembungkus RAII sudah ada. Pemanggilan
std::fclosetambahan menyebabkan destructor menutup berkas yang sudah ditutup. Biarkan satu pihak saja yang bertanggung jawab, yaitu objek pembungkusnya. - Memakai pointer mentah dari
get()setelah objek RAII tamat. RAII hanya mengatur sumber daya milik objeknya, sedangkan salinan alamatnya tetap menjadi pointer yang menggantung ketika objeknya sudah dihancurkan. Simpan dan gunakan objek pembungkusnya, bukan alamat sementaranya.
Ringkasan#
- RAII memasangkan perolehan sumber daya dengan constructor dan pelepasannya dengan destructor, sehingga pelepasan berjalan otomatis di setiap jalur keluar, termasuk saat exception.
- Pelepasan ditulis satu kali di destructor, bukan di setiap pernyataan
return, sehingga fungsi tidak kehilangan kebenarannya setiap kali ditambah jalur keluar baru. std::vector,std::string,std::fstream,std::lock_guard, dan smart pointer semuanya adalah kelas RAII dari pustaka standar.
Referensi#
- RAII — cppreference.com — Definisi RAII, contoh pembungkus mutex, dan penjelasan jaminannya saat stack unwinding.
- Destructors — LearnCpp — Cara kerja destructor dan urutan pemanggilannya ketika objek keluar dari lingkup.
- C++ FAQ: Destructors — isocpp.org — Kumpulan jawaban resmi komunitas C++ tentang destructor dan kaitannya dengan RAII.