RAII dan smart pointer di C ++

193

Dalam praktiknya dengan C ++, apa itu RAII , apa itu smart pointer , bagaimana penerapannya dalam sebuah program dan apa manfaat menggunakan RAII dengan smart pointer?

Rob Kam
sumber

Jawaban:

317

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.

Michael Williamson
sumber
7
Perlu dicatat bahwa banyak implementasi string diimplementasikan dalam hal pointer yang dihitung referensi. Semantik copy-on-write ini membuat mengembalikan sebuah string dengan nilai sangat murah.
7
Bahkan untuk yang tidak, banyak kompiler menerapkan optimasi NRV yang akan menangani overhead. Secara umum, saya menemukan shared_ptr jarang bermanfaat - cukup gunakan RAII dan hindari kepemilikan bersama.
Nemanja Trifunovic
27
mengembalikan string bukanlah alasan yang baik untuk menggunakan pointer pintar. optimasi nilai kembali dapat dengan mudah mengoptimalkan pengembalian, dan c ++ 1x memindahkan semantik akan menghilangkan salinan sama sekali (bila digunakan dengan benar). Tunjukkan beberapa contoh dunia nyata (misalnya saat kami berbagi sumber daya yang sama) :)
Johannes Schaub - litb
1
Saya pikir kesimpulan Anda sejak awal tentang mengapa Java tidak bisa melakukan ini tidak memiliki kejelasan. Cara termudah untuk menggambarkan batasan ini di Java atau C # adalah karena tidak ada cara untuk mengalokasikan pada stack. C # memungkinkan alokasi tumpukan melalui kata kunci khusus namun, Anda kehilangan jenis keselamatan.
ApplePieIsGood
4
@Nemanja Trifunovic: Oleh RAII dalam konteks ini maksud Anda mengembalikan salinan / membuat objek di tumpukan? Itu tidak berfungsi jika Anda telah mengembalikan / menerima objek tipe yang dapat disubklasifikasikan. Maka Anda harus menggunakan pointer untuk menghindari mengiris objek, dan saya berpendapat bahwa pointer cerdas seringkali lebih baik daripada yang mentah dalam kasus tersebut.
Frank Osterfeld
141

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 deleteketika 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:

unique_ptr

adalah pointer cerdas yang memiliki objek secara eksklusif. Itu tidak dalam peningkatan, tetapi kemungkinan akan muncul di C ++ Standard berikutnya. Ini tidak dapat disalin tetapi mendukung transfer kepemilikan . Beberapa contoh kode (C ++ berikutnya):

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.

scoped_ptr

adalah penambah smart pointer yang tidak dapat disalin atau dipindah. Ini adalah hal yang sempurna untuk digunakan ketika Anda ingin memastikan pointer dihapus ketika keluar dari ruang lingkup.

Kode:

void do_something() {
    scoped_ptr<pipe> sp(new pipe);
    // do something here...
} // when going out of scope, sp will delete the pointer automatically. 

shared_ptr

adalah untuk kepemilikan bersama. Oleh karena itu, keduanya dapat disalin dan bergerak. Beberapa instance smart pointer dapat memiliki sumber daya yang sama. Segera setelah smart pointer terakhir yang memiliki sumber daya keluar dari ruang lingkup, sumber daya akan dibebaskan. Beberapa contoh dunia nyata dari salah satu proyek saya:

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.

Johannes Schaub - litb
sumber
Sejauh yang saya tahu benda yang tidak dapat disalin tidak baik digunakan sama sekali dalam wadah stl karena mereka bergantung pada nilai semantik - apa yang terjadi jika Anda ingin menyortir wadah itu? semacam melakukan elemen copy ...
fmuecke
Wadah C ++ 0x akan diubah sehingga menghormati tipe bergerak saja unique_ptr, dan sortjuga akan berubah.
Johannes Schaub - litb
Apakah Anda ingat di mana Anda pertama kali mendengar istilah SBRM? James berusaha melacaknya.
GManNickG
header atau perpustakaan mana yang harus saya sertakan untuk menggunakannya? ada bacaan lebih lanjut tentang ini?
atoMerz
Satu saran di sini: jika ada jawaban untuk pertanyaan C ++ oleh @litb, itu adalah jawaban yang tepat (tidak peduli suara atau jawaban yang ditandai "benar") ...
fnl
32

Premis dan alasannya sederhana, dalam konsep.

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.

Drew Dormann
sumber
Selain itu - pointer adalah aplikasi paling umum dari RAII - Anda kemungkinan akan mengalokasikan ribuan kali lebih banyak pointer daripada sumber daya lainnya.
Eclipse
8

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:

  1. Kami mengalokasikan memori sebelum kami menggunakannya, selalu, bahkan ketika kami merasa tidak menyukainya - sulit untuk melakukan cara lain dengan smart pointer. Jika ini tidak terjadi, Anda akan mencoba mengakses memori NULL, yang mengakibatkan crash (sangat menyakitkan).
  2. Kami membebaskan memori bahkan ketika ada kesalahan. Tidak ada memori yang tersisa menggantung.

Misalnya, contoh lain adalah soket jaringan RAII. Pada kasus ini:

  1. Kami membuka soket jaringan sebelum menggunakannya, selalu, bahkan ketika kami merasa tidak suka - sulit untuk melakukannya dengan RAII. Jika Anda mencoba melakukan ini tanpa RAII Anda dapat membuka soket kosong untuk, katakan koneksi MSN. Maka pesan seperti "ayo lakukan malam ini" mungkin tidak ditransfer, pengguna tidak akan bercinta, dan Anda mungkin berisiko dipecat.
  2. Kami menutup soket jaringan bahkan ketika ada kesalahan. Tidak ada soket yang dibiarkan menggantung karena ini dapat mencegah pesan respons "yakin sakit berada di bawah" dari memukul pengirim kembali.

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.

mannicken
sumber
2

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 deletesepotong memori, jangan sampai Anda berakhir dengan kebocoran memori, atau pointer yang keliru dibebaskan dua kali dan dapat merusak seluruh tumpukan.

Jason S
sumber
0
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.


sumber
1
void f () {Obj x; } Obj x akan dihapus melalui penciptaan / penghancuran bingkai stack (unwinding) ... itu tidak terkait dengan penghitungan referensi.
Hernán
Penghitungan referensi adalah fitur dari implementasi internal string. RAII adalah konsep di balik penghapusan objek ketika objek keluar dari cakupan. Pertanyaannya adalah tentang RAII dan juga pointer cerdas.
1
"Tidak peduli apa yang terjadi" - apa yang terjadi jika pengecualian dilemparkan sebelum fungsi kembali?
titaniumdecoy
Fungsi mana yang dikembalikan? Jika pengecualian dilemparkan ke foo, maka bilah dihapus. Konstruktor default untuk melempar pengecualian akan menjadi peristiwa luar biasa.