Dalam praktiknya dengan C ++, apa itu RAII , apa itu smart pointer , bagaimana penerapannya dalam sebuah program dan apa manfaat menggunakan RAII dengan smart pointer?
Dalam praktiknya dengan C ++, apa itu RAII , apa itu smart pointer , bagaimana penerapannya dalam sebuah program dan apa manfaat menggunakan RAII dengan smart pointer?
Jawaban:
Contoh RAII yang sederhana (dan mungkin terlalu sering digunakan) adalah kelas File. Tanpa RAII, kodenya mungkin terlihat seperti ini:
File file("/path/to/file");
// Do stuff with file
file.close();
Dengan kata lain, kita harus memastikan bahwa kita menutup file setelah selesai. Ini memiliki dua kelemahan - pertama, di mana pun kita menggunakan File, kita harus memanggil File :: close () - jika kita lupa melakukan ini, kita memegang file lebih lama dari yang seharusnya. Masalah kedua adalah bagaimana jika pengecualian dilemparkan sebelum kita menutup file?
Java memecahkan masalah kedua menggunakan klausa akhirnya:
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
atau sejak Java 7, pernyataan coba-dengan-sumber daya:
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++ memecahkan kedua masalah menggunakan RAII - yaitu, menutup file di destructor File. Selama objek File dihancurkan pada waktu yang tepat (yang seharusnya demikian), menutup file akan diurus untuk kita. Jadi, kode kita sekarang terlihat seperti:
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
Ini tidak dapat dilakukan di Jawa karena tidak ada jaminan kapan objek akan dihancurkan, jadi kami tidak dapat menjamin kapan sumber daya seperti file akan dibebaskan.
Ke smart pointer - sering kali, kami hanya membuat objek di stack. Misalnya (dan mencuri contoh dari jawaban lain):
void foo() {
std::string str;
// Do cool things to or using str
}
Ini berfungsi dengan baik - tetapi bagaimana jika kita ingin mengembalikan str? Kita bisa menulis ini:
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
Jadi, apa yang salah dengan itu? Nah, tipe kembalinya adalah std :: string - jadi itu artinya kita mengembalikan berdasarkan nilai. Ini berarti bahwa kami menyalin str dan benar-benar mengembalikan salinan. Ini bisa mahal, dan kami mungkin ingin menghindari biaya menyalinnya. Oleh karena itu, kami mungkin datang dengan ide untuk kembali dengan referensi atau dengan pointer.
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
Sayangnya, kode ini tidak berfungsi. Kami mengembalikan pointer ke str - tetapi str dibuat di stack, jadi kami dihapus setelah kami keluar dari foo (). Dengan kata lain, pada saat penelepon mendapatkan pointer, itu tidak berguna (dan bisa dibilang lebih buruk daripada tidak berguna karena menggunakannya dapat menyebabkan segala macam kesalahan yang funky)
Jadi, apa solusinya? Kita bisa membuat str di heap menggunakan yang baru - dengan cara itu, ketika foo () selesai, str tidak akan dihancurkan.
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
Tentu saja, solusi ini juga tidak sempurna. Alasannya adalah karena kami telah membuat str, tetapi kami tidak pernah menghapusnya. Ini mungkin bukan masalah dalam program yang sangat kecil, tetapi secara umum, kami ingin memastikan kami menghapusnya. Kami hanya bisa mengatakan bahwa penelepon harus menghapus objek begitu dia selesai. Kelemahannya adalah bahwa penelepon harus mengelola memori, yang menambah kompleksitas ekstra, dan mungkin salah, menyebabkan kebocoran memori yaitu tidak menghapus objek meskipun tidak lagi diperlukan.
Di sinilah pointer pintar masuk. Contoh berikut menggunakan shared_ptr - Saya sarankan Anda melihat berbagai jenis pointer pintar untuk mempelajari apa yang sebenarnya ingin Anda gunakan.
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
Sekarang, shared_ptr akan menghitung jumlah referensi ke str. Misalnya
shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;
Sekarang ada dua referensi ke string yang sama. Setelah tidak ada referensi tersisa untuk str, itu akan dihapus. Dengan demikian, Anda tidak perlu lagi khawatir menghapusnya sendiri.
Sunting cepat: seperti yang ditunjukkan beberapa komentar, contoh ini tidak sempurna untuk (setidaknya!) Dua alasan. Pertama, karena penerapan string, menyalin string cenderung tidak mahal. Kedua, karena apa yang dikenal sebagai optimasi nilai pengembalian, pengembalian dengan nilai mungkin tidak mahal karena kompiler dapat melakukan beberapa kepintaran untuk mempercepat segalanya.
Jadi, mari kita coba contoh berbeda menggunakan kelas File kita.
Katakanlah kita ingin menggunakan file sebagai log. Ini berarti kami ingin membuka file kami dalam mode tambahkan saja:
File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log
Sekarang, mari kita atur file kita sebagai log untuk beberapa objek lain:
void setLog(const Foo & foo, const Bar & bar) {
File file("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Sayangnya, contoh ini berakhir mengerikan - file akan ditutup segera setelah metode ini berakhir, artinya foo dan bar sekarang memiliki file log yang tidak valid. Kita dapat membuat file di heap, dan meneruskan pointer ke file ke foo dan bar:
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Tapi lalu siapa yang bertanggung jawab untuk menghapus file? Jika tidak menghapus file, maka kami memiliki kebocoran memori dan sumber daya. Kami tidak tahu apakah foo atau bar akan selesai dengan file terlebih dahulu, jadi kami tidak dapat berharap untuk menghapus file itu sendiri. Misalnya, jika foo menghapus file sebelum bar selesai dengan itu, bar sekarang memiliki pointer yang tidak valid.
Jadi, seperti yang sudah Anda duga, kami bisa menggunakan smart pointer untuk membantu kami.
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Sekarang, tidak ada yang perlu khawatir tentang menghapus file - setelah foo dan bar selesai dan tidak lagi memiliki referensi ke file (mungkin karena foo dan bar sedang dihancurkan), file akan secara otomatis dihapus.
RAII Ini adalah nama yang aneh untuk konsep yang sederhana namun mengagumkan. Lebih baik namanya Scope Bound Resource Management (SBRM). Idenya adalah sering kali Anda mengalokasikan sumber daya di awal blok, dan perlu melepaskannya di pintu keluar blok. Keluar dari blok dapat terjadi dengan kontrol aliran normal, melompat keluar darinya, dan bahkan dengan pengecualian. Untuk mencakup semua kasus ini, kode menjadi lebih rumit dan berlebihan.
Contoh saja melakukannya tanpa SBRM:
void o_really() {
resource * r = allocate_resource();
try {
// something, which could throw. ...
} catch(...) {
deallocate_resource(r);
throw;
}
if(...) { return; } // oops, forgot to deallocate
deallocate_resource(r);
}
Seperti yang Anda lihat ada banyak cara yang bisa kita dapatkan. Idenya adalah bahwa kami merangkum manajemen sumber daya ke dalam kelas. Inisialisasi objeknya memperoleh sumber daya ("Akuisisi Sumber Daya Adalah Inisialisasi"). Pada saat kita keluar dari blok (ruang lingkup blok), sumber daya dibebaskan lagi.
struct resource_holder {
resource_holder() {
r = allocate_resource();
}
~resource_holder() {
deallocate_resource(r);
}
resource * r;
};
void o_really() {
resource_holder r;
// something, which could throw. ...
if(...) { return; }
}
Itu bagus jika Anda memiliki kelas sendiri yang tidak semata-mata untuk tujuan mengalokasikan / menghapus alokasi sumber daya. Alokasi hanya akan menjadi perhatian tambahan untuk menyelesaikan pekerjaan mereka. Tetapi begitu Anda hanya ingin mengalokasikan / mengalokasikan sumber daya, hal di atas menjadi tidak lancar. Anda harus menulis kelas pembungkus untuk setiap jenis sumber daya yang Anda peroleh. Untuk memudahkan itu, smart pointer memungkinkan Anda untuk mengotomatiskan proses itu:
shared_ptr<Entry> create_entry(Parameters p) {
shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
return e;
}
Biasanya, smart pointer adalah pembungkus tipis di sekitar baru / hapus yang kebetulan memanggil delete
ketika sumber daya yang mereka miliki keluar dari ruang lingkup. Beberapa pointer cerdas, seperti shared_ptr memungkinkan Anda memberi tahu mereka yang disebut deleter, yang digunakan sebagai ganti delete
. Itu memungkinkan Anda, misalnya, untuk mengelola pegangan jendela, sumber daya ekspresi reguler, dan hal-hal sewenang-wenang lainnya, selama Anda memberi tahu shared_ptr tentang deleter yang tepat.
Ada berbagai petunjuk cerdas untuk tujuan yang berbeda:
Kode:
unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u
vector<unique_ptr<plot_src>> pv;
pv.emplace_back(new plot_src);
pv.emplace_back(new plot_src);
Tidak seperti auto_ptr, unique_ptr dapat dimasukkan ke dalam wadah, karena wadah akan dapat menampung jenis yang tidak dapat disalin (tetapi dapat dipindahkan), seperti aliran dan unique_ptr juga.
Kode:
void do_something() {
scoped_ptr<pipe> sp(new pipe);
// do something here...
} // when going out of scope, sp will delete the pointer automatically.
Kode:
shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and
// plot2 both still have references.
Seperti yang Anda lihat, sumber-plot (fungsi fx) dibagi, tetapi masing-masing memiliki entri yang terpisah, di mana kami mengatur warna. Ada kelas lemah_ptr yang digunakan ketika kode perlu merujuk ke sumber daya yang dimiliki oleh penunjuk pintar, tetapi tidak perlu memiliki sumber daya. Alih-alih melewati pointer mentah, Anda harus membuat lemah_ptr. Ini akan melempar pengecualian ketika pemberitahuan Anda mencoba mengakses sumber daya dengan jalur akses lemah_ptr, meskipun tidak ada shared_ptr lagi yang memiliki sumber daya.
unique_ptr
, dan sort
juga akan berubah.
RAII adalah paradigma desain untuk memastikan bahwa variabel menangani semua inisialisasi yang diperlukan dalam konstruktor mereka dan semua pembersihan yang diperlukan dalam destruktor mereka. Ini mengurangi semua inisialisasi dan pembersihan menjadi satu langkah.
C ++ tidak memerlukan RAII, tetapi semakin diterima bahwa menggunakan metode RAII akan menghasilkan kode yang lebih kuat.
Alasan bahwa RAII berguna dalam C ++ adalah bahwa C ++ secara intrinsik mengelola penciptaan dan penghancuran variabel ketika mereka memasuki dan meninggalkan ruang lingkup, baik melalui aliran kode normal atau melalui tumpukan yang tidak terpicu yang dipicu oleh pengecualian. Itu freebie di C ++.
Dengan mengikat semua inisialisasi dan pembersihan ke mekanisme ini, Anda dipastikan bahwa C ++ akan menangani pekerjaan ini untuk Anda juga.
Berbicara tentang RAII di C ++ biasanya mengarah ke diskusi tentang smart pointer, karena pointer sangat rapuh ketika datang ke pembersihan. Ketika mengelola heap-dialokasikan memori yang diperoleh dari malloc atau baru, biasanya merupakan tanggung jawab programmer untuk membebaskan atau menghapus memori itu sebelum pointer dihancurkan. Pointer pintar akan menggunakan filosofi RAII untuk memastikan bahwa objek yang dialokasikan tumpukan dihancurkan setiap saat variabel pointer dihancurkan.
Smart pointer adalah variasi dari RAII. RAII berarti akuisisi sumber daya adalah inisialisasi. Smart pointer memperoleh sumber daya (memori) sebelum digunakan dan kemudian membuangnya secara otomatis di destruktor. Dua hal terjadi:
Misalnya, contoh lain adalah soket jaringan RAII. Pada kasus ini:
Sekarang, seperti yang Anda lihat, RAII adalah alat yang sangat berguna dalam banyak kasus karena membantu orang untuk bercinta.
Sumber C ++ dari pointer cerdas ada jutaan di seluruh internet termasuk tanggapan di atas saya.
Boost memiliki sejumlah ini termasuk yang ada di Boost.Interprocess untuk memori bersama. Ini sangat menyederhanakan manajemen memori, terutama dalam situasi yang memicu sakit kepala seperti ketika Anda memiliki 5 proses berbagi struktur data yang sama: ketika semua orang selesai dengan sepotong memori, Anda ingin itu secara otomatis dibebaskan & tidak harus duduk di sana mencoba mencari tahu siapa yang harus bertanggung jawab untuk memanggil delete
sepotong memori, jangan sampai Anda berakhir dengan kebocoran memori, atau pointer yang keliru dibebaskan dua kali dan dapat merusak seluruh tumpukan.
batal foo () { std :: string bar; // // lebih banyak kode di sini // }
Apa pun yang terjadi, bilah akan dihapus dengan benar begitu ruang lingkup fungsi foo () telah ditinggalkan.
Implementasi string std :: string internal sering menggunakan referensi penghitung referensi. Jadi string internal hanya perlu disalin ketika salah satu salinan string berubah. Oleh karena itu referensi yang dihitung smart pointer memungkinkan untuk hanya menyalin sesuatu saat diperlukan.
Selain itu, penghitungan referensi internal memungkinkan memori akan dihapus dengan benar ketika salinan string internal tidak lagi diperlukan.