Apakah C ++ mendukung 'akhirnya' blok? (Dan apa 'RAII' yang terus kudengar?)

Jawaban:

273

Tidak, C ++ tidak mendukung blok 'akhirnya'. Alasannya adalah bahwa C ++ sebaliknya mendukung RAII: "Akuisisi Sumber Daya Adalah Inisialisasi" - nama yang buruk untuk konsep yang sangat berguna.

Idenya adalah bahwa destruktor suatu objek bertanggung jawab untuk membebaskan sumber daya. Ketika objek memiliki durasi penyimpanan otomatis, destruktor objek akan dipanggil ketika blok tempat ia dibuat keluar - bahkan ketika blok itu keluar di hadapan pengecualian. Inilah penjelasan Bjarne Stroustrup tentang topik tersebut.

Penggunaan umum untuk RAII adalah mengunci mutex:

// A class with implements RAII
class lock
{
    mutex &m_;

public:
    lock(mutex &m)
      : m_(m)
    {
        m.acquire();
    }
    ~lock()
    {
        m_.release();
    }
};

// A class which uses 'mutex' and 'lock' objects
class foo
{
    mutex mutex_; // mutex for locking 'foo' object
public:
    void bar()
    {
        lock scopeLock(mutex_); // lock object.

        foobar(); // an operation which may throw an exception

        // scopeLock will be destructed even if an exception
        // occurs, which will release the mutex and allow
        // other functions to lock the object and run.
    }
};

RAII juga menyederhanakan menggunakan objek sebagai anggota kelas lain. Ketika kelas pemilik 'dihancurkan, sumber daya yang dikelola oleh kelas RAII dilepaskan karena destruktor untuk kelas yang dikelola RAII dipanggil sebagai hasilnya. Ini berarti bahwa ketika Anda menggunakan RAII untuk semua anggota di kelas yang mengelola sumber daya, Anda bisa lolos dengan menggunakan destruktor yang sangat sederhana, bahkan mungkin default, untuk kelas pemilik karena tidak perlu mengelola masa pakai sumber daya anggotanya secara manual. . (Terima kasih kepada Mike B untuk menunjukkan ini.)

Bagi mereka yang akrab dengan C # atau VB.NET, Anda mungkin mengakui bahwa RAII mirip dengan kehancuran deterministik .NET menggunakan pernyataan IDisposable dan 'using' . Memang, kedua metode ini sangat mirip. Perbedaan utama adalah bahwa RAII akan secara pasti melepaskan semua jenis sumber daya - termasuk memori. Saat menerapkan IDisposable di .NET (bahkan bahasa .NET C ++ / CLI), sumber daya akan dirilis secara deterministik kecuali untuk memori. Di .NET, memori tidak dilepaskan secara deterministik; memori hanya dilepaskan selama siklus pengumpulan sampah.

 

† Beberapa orang percaya bahwa "Penghancuran adalah Pelepasan Sumber Daya" adalah nama yang lebih akurat untuk idiom RAII.

Kevin
sumber
18
"Penghancuran adalah Pelepasan Sumber Daya" - DIRR ... Tidak, tidak bekerja untuk saya. = P
Erik Forbes
14
RAII macet - benar-benar tidak ada perubahan. Berusaha melakukan itu adalah tindakan bodoh. Namun, Anda harus mengakui bahwa "Akuisisi Sumber Daya Adalah Inisialisasi" masih merupakan nama yang cukup buruk.
Kevin
162
SBRM == Manajemen Sumber Daya Lingkup Terikat
Johannes Schaub - litb
10
Siapa pun yang memiliki keahlian untuk merekayasa tidak hanya perangkat lunak pada umumnya, apalagi teknik yang ditingkatkan, tidak dapat memberikan alasan yang layak untuk akronim yang mengerikan itu.
Hardryv
54
Ini membuat Anda macet ketika Anda memiliki sesuatu untuk dibersihkan yang tidak cocok dengan masa hidup objek C ++ mana pun. Saya kira Anda berakhir dengan Lifetime Equals C ++ Class Liftime Atau Else It Get Ugly (LECCLEOEIGU?).
Warren P
79

Dalam C ++ akhirnya TIDAK diperlukan karena RAII.

RAII memindahkan tanggung jawab keselamatan pengecualian dari pengguna objek ke perancang (dan pelaksana) objek. Saya berpendapat ini adalah tempat yang tepat karena Anda hanya perlu mendapatkan pengecualian keselamatan yang benar sekali (dalam desain / implementasi). Dengan menggunakan akhirnya Anda harus mendapatkan pengecualian keselamatan yang benar setiap kali Anda menggunakan objek.

Juga IMO kodenya terlihat lebih rapi (lihat di bawah).

Contoh:

Objek basis data. Untuk memastikan koneksi DB digunakan, itu harus dibuka dan ditutup. Dengan menggunakan RAII ini dapat dilakukan di konstruktor / destruktor.

C ++ Suka RAII

void someFunc()
{
    DB    db("DBDesciptionString");
    // Use the db object.

} // db goes out of scope and destructor closes the connection.
  // This happens even in the presence of exceptions.

Penggunaan RAII membuat menggunakan objek DB dengan benar sangat mudah. Objek DB akan benar menutup dirinya dengan menggunakan destruktor tidak peduli bagaimana kami mencoba dan menyalahgunakannya.

Akhirnya Java

void someFunc()
{
    DB      db = new DB("DBDesciptionString");
    try
    {
        // Use the db object.
    }
    finally
    {
        // Can not rely on finaliser.
        // So we must explicitly close the connection.
        try
        {
            db.close();
        }
        catch(Throwable e)
        {
           /* Ignore */
           // Make sure not to throw exception if one is already propagating.
        }
    }
}

Saat menggunakan akhirnya penggunaan yang benar dari objek didelegasikan kepada pengguna objek. yaitu adalah tanggung jawab pengguna objek untuk secara benar menutup koneksi DB secara eksplisit. Sekarang Anda dapat berargumen bahwa ini dapat dilakukan di finaliser, tetapi sumber daya mungkin memiliki ketersediaan terbatas atau kendala lain dan dengan demikian Anda umumnya ingin mengontrol pelepasan objek dan tidak bergantung pada perilaku non deterministik dari pengumpul sampah.

Ini juga contoh sederhana.
Ketika Anda memiliki banyak sumber daya yang perlu dirilis kode dapat menjadi rumit.

Analisis yang lebih terperinci dapat ditemukan di sini: http://accu.org/index.php/journals/236

Martin York
sumber
16
// Make sure not to throw exception if one is already propagating.Penting bagi penghancur C ++ untuk tidak membuang pengecualian juga karena alasan ini.
Cemafor
10
@Emafor: Alasan C ++ untuk tidak membuang pengecualian dari destructor berbeda dari Java. Di Jawa itu akan berfungsi (Anda hanya kehilangan pengecualian asli). Dalam C ++ sangat buruk. Tetapi intinya di C ++ adalah bahwa Anda hanya perlu melakukannya sekali (oleh perancang kelas) ketika ia menulis destruktor. Di Jawa Anda harus melakukannya di titik penggunaan. Jadi itu adalah tanggung jawab pengguna kelas untuk menulis plat ketel yang sama tepat waktu.
Martin York
1
Jika ini soal "dibutuhkan", Anda tidak perlu RAII juga. Mari kita singkirkan itu! :-) Selain lelucon, RAII baik-baik saja untuk banyak kasus. Apa yang dilakukan RAII membuat lebih rumit adalah kasus ketika Anda ingin mengeksekusi beberapa kode (tidak terkait sumber daya) bahkan jika kode di atas kembali lebih awal. Untuk itu Anda menggunakan gotos atau memisahkannya menjadi dua metode.
Trinidad
1
@ Tridad: Ini tidak sesederhana yang Anda pikirkan (karena semua saran Anda tampaknya memilih opsi terburuk yang mungkin). Itulah sebabnya pertanyaan mungkin menjadi tempat yang lebih baik untuk mengeksplorasi ini daripada komentar.
Martin York
1
Mengkritik "TIDAK diperlukan karena RAII": ada banyak kasus di mana menambahkan RAII ad-hoc akan terlalu banyak kode boilerplate untuk ditambahkan, dan coba-akhirnya akan sangat tepat.
ceztko
63

RAII biasanya lebih baik, tetapi Anda dapat dengan mudah memiliki semantik terakhir di C ++. Menggunakan sejumlah kecil kode.

Selain itu, Panduan Inti C ++ akhirnya memberi.

Berikut adalah tautan ke implementasi Microsoft GSL dan tautan ke penerapan Martin Moene

Bjarne Stroustrup beberapa kali mengatakan bahwa segala sesuatu yang ada di GSL artinya akan masuk standar pada akhirnya. Jadi itu harus menjadi cara masa depan-bukti untuk menggunakan akhirnya .

Anda dapat dengan mudah menerapkan diri sendiri jika ingin, lanjutkan membaca.

Dalam C ++ 11 RAII dan lambdas memungkinkan untuk membuat jenderal akhirnya:

namespace detail { //adapt to your "private" namespace
template <typename F>
struct FinalAction {
    FinalAction(F f) : clean_{f} {}
   ~FinalAction() { if(enabled_) clean_(); }
    void disable() { enabled_ = false; };
  private:
    F clean_;
    bool enabled_{true}; }; }

template <typename F>
detail::FinalAction<F> finally(F f) {
    return detail::FinalAction<F>(f); }

contoh penggunaan:

#include <iostream>
int main() {
    int* a = new int;
    auto delete_a = finally([a] { delete a; std::cout << "leaving the block, deleting a!\n"; });
    std::cout << "doing something ...\n"; }

hasilnya adalah:

doing something...
leaving the block, deleting a!

Secara pribadi saya menggunakan ini beberapa kali untuk memastikan untuk menutup deskriptor file POSIX dalam program C ++.

Memiliki kelas nyata yang mengelola sumber daya dan menghindari segala jenis kebocoran biasanya lebih baik, tetapi ini akhirnya berguna dalam kasus di mana membuat kelas terdengar seperti pembunuhan berlebihan.

Selain itu, aku seperti itu lebih baik daripada bahasa lain akhirnya karena jika digunakan secara alami Anda menulis kode penutupan terdekat kode pembuka (dalam contoh saya baru dan delete ) dan kehancuran berikut konstruksi dalam rangka LIFO seperti biasa di C ++. Satu-satunya downside adalah bahwa Anda mendapatkan variabel otomatis Anda tidak benar-benar menggunakan dan sintaks lambda membuatnya sedikit bising (dalam contoh saya di baris keempat hanya kata akhirnya dan {} -block di sebelah kanan bermakna, the sisanya pada dasarnya berisik).

Contoh lain:

 [...]
 auto precision = std::cout.precision();
 auto set_precision_back = finally( [precision, &std::cout]() { std::cout << std::setprecision(precision); } );
 std::cout << std::setprecision(3);

Anggota yang dinonaktifkan berguna jika yang terakhir harus dipanggil hanya jika terjadi kegagalan. Misalnya, Anda harus menyalin objek dalam tiga wadah berbeda, Anda dapat mengatur akhirnya untuk membatalkan setiap salinan dan menonaktifkan setelah semua salinan berhasil. Melakukan hal itu, jika kehancuran tidak dapat terjadi, Anda memastikan jaminan yang kuat.

nonaktifkan contoh:

//strong guarantee
void copy_to_all(BIGobj const& a) {
    first_.push_back(a);
    auto undo_first_push = finally([first_&] { first_.pop_back(); });

    second_.push_back(a);
    auto undo_second_push = finally([second_&] { second_.pop_back(); });

    third_.push_back(a);
    //no necessary, put just to make easier to add containers in the future
    auto undo_third_push = finally([third_&] { third_.pop_back(); });

    undo_first_push.disable();
    undo_second_push.disable();
    undo_third_push.disable(); }

Jika Anda tidak dapat menggunakan C ++ 11 Anda masih dapat memiliki akhirnya , tetapi kode menjadi sedikit lebih panjang lebar. Cukup tentukan struct dengan hanya konstruktor dan destruktor, konstruktor mengambil referensi untuk apa pun yang diperlukan dan destruktor melakukan tindakan yang Anda butuhkan. Ini pada dasarnya adalah apa yang dilakukan lambda, dilakukan secara manual.

#include <iostream>
int main() {
    int* a = new int;

    struct Delete_a_t {
        Delete_a_t(int* p) : p_(p) {}
       ~Delete_a_t() { delete p_; std::cout << "leaving the block, deleting a!\n"; }
        int* p_;
    } delete_a(a);

    std::cout << "doing something ...\n"; }
Paolo.Bolzoni
sumber
Mungkin ada masalah yang mungkin terjadi: pada fungsi 'akhirnya (Ff)' ia mengembalikan objek FinalAction, sehingga dekonstruktor mungkin dipanggil sebelum akhirnya mengembalikan fungsi. Mungkin kita harus menggunakan std :: function daripada template F.
user1633272
Perhatikan bahwa FinalActionpada dasarnya sama dengan ScopeGuardidiom populer , hanya dengan nama yang berbeda.
anderas
1
Apakah pengoptimalan ini aman?
Nulano
2
@ Paolo.Bolzoni Maaf karena tidak membalas lebih cepat, saya tidak mendapatkan pemberitahuan atas komentar Anda. Saya khawatir akhirnya blok (di mana saya memanggil fungsi DLL) akan dipanggil sebelum akhir lingkup (karena variabel tidak digunakan), tetapi sejak itu menemukan pertanyaan di SO yang membersihkan kekhawatiran saya. Saya akan menautkannya, tetapi sayangnya, saya tidak dapat menemukannya lagi.
Nulano
1
Fungsi disable () adalah semacam kutil pada desain Anda yang sebelumnya bersih. Jika Anda ingin akhirnya hanya dipanggil jika gagal, lalu mengapa tidak menggunakan pernyataan menangkap? Bukankah itu untuk apa?
user2445507
32

Selain memudahkan pembersihan dengan objek berbasis tumpukan, RAII juga berguna karena pembersihan 'otomatis' yang sama terjadi ketika objek tersebut adalah anggota kelas lain. Ketika kelas pemilik dihancurkan, sumber daya yang dikelola oleh kelas RAII akan dibersihkan karena dtor untuk kelas itu dipanggil sebagai hasilnya.

Ini berarti bahwa ketika Anda mencapai RAII nirwana dan semua anggota di kelas menggunakan RAII (seperti smart pointer), Anda bisa lolos dengan dtor yang sangat sederhana (bahkan mungkin default) untuk kelas pemilik karena tidak perlu mengelola secara manual masa hidup sumber daya anggota.

Michael Burr
sumber
Itu poin yang sangat bagus. Memberi +1 kepada Anda. Tidak banyak orang yang memilih Anda. Saya harap Anda tidak keberatan bahwa saya mengedit posting saya untuk memasukkan komentar Anda. (Tentu saja saya memberi Anda kredit.) Terima kasih! :)
Kevin
30

mengapa bahkan bahasa yang dikelola memberikan blok akhirnya meskipun sumber daya secara otomatis dialokasikan oleh pengumpul sampah?

Sebenarnya, bahasa berdasarkan pengumpul Sampah perlu "akhirnya" lebih banyak. Seorang pengumpul sampah tidak menghancurkan objek Anda secara tepat waktu, sehingga tidak dapat diandalkan untuk membersihkan masalah yang berhubungan dengan non-memori dengan benar.

Dalam hal data yang dialokasikan secara dinamis, banyak yang akan berpendapat bahwa Anda harus menggunakan smart-pointer.

Namun...

RAII memindahkan tanggung jawab keselamatan pengecualian dari pengguna objek ke perancang

Sayangnya ini adalah kejatuhannya sendiri. Kebiasaan pemrograman C lama sangat sulit. Saat Anda menggunakan perpustakaan yang ditulis dalam gaya C atau sangat C, RAII tidak akan pernah digunakan. Pendek menulis ulang seluruh API front-end, itu hanya apa yang harus Anda kerjakan. Maka kurangnya "akhirnya" benar-benar menggigit.

Philip Couling
sumber
13
Tepat ... RAII terlihat bagus dari sudut pandang yang ideal. Tapi saya harus bekerja dengan API C konvensional sepanjang waktu (seperti fungsi C-style di Win32 API ...). Sangat umum untuk mendapatkan sumber daya yang mengembalikan semacam HANDLE, yang kemudian membutuhkan beberapa fungsi seperti CloseHandle (HANDLE) untuk membersihkan. Menggunakan try ... akhirnya adalah cara yang bagus untuk berurusan dengan kemungkinan pengecualian. (Syukurlah, sepertinya shared_ptr dengan pengukur khusus dan lambda C ++ 11 harus menyediakan beberapa bantuan berbasis RAII yang tidak mengharuskan penulisan seluruh kelas untuk membungkus API yang hanya saya gunakan di satu tempat.).
James Johnston
7
@ JamesJohnston, sangat mudah untuk menulis kelas pembungkus yang memegang segala jenis pegangan dan menyediakan mekanik RAII. ATL menyediakan banyak dari mereka misalnya. Sepertinya Anda menganggap ini terlalu banyak masalah, tetapi saya tidak setuju, mereka sangat kecil dan mudah ditulis.
Mark Ransom
5
Sederhana ya, tidak kecil. Ukuran tergantung pada kompleksitas perpustakaan yang Anda gunakan.
Philip Couling
1
@MarkRansom: Apakah ada mekanisme melalui mana RAII dapat melakukan sesuatu yang cerdas jika pengecualian terjadi selama pembersihan sementara pengecualian lain sedang menunggu? Dalam sistem dengan coba / akhirnya, dimungkinkan - meskipun canggung - untuk mengatur berbagai hal sehingga pengecualian yang tertunda dan pengecualian yang terjadi selama pembersihan keduanya disimpan dalam yang baru CleanupFailedException. Apakah ada cara yang masuk akal untuk mencapai hasil seperti itu menggunakan RAII?
supercat
3
@couling: Ada banyak kasus di mana suatu program akan memanggil SomeObject.DoSomething()metode dan ingin tahu apakah itu (1) berhasil, (2) gagal tanpa efek samping , (3) gagal dengan efek samping pemanggil siap untuk mengatasinya , atau (4) gagal dengan efek samping pemanggil tidak dapat mengatasinya. Hanya penelepon yang akan tahu situasi apa yang bisa dan tidak bisa diatasi; apa yang penelepon butuhkan adalah cara untuk mengetahui apa situasinya. Sayang sekali tidak ada mekanisme standar untuk memasok informasi paling penting tentang pengecualian.
supercat
9

Lain "akhirnya" emulasi blok menggunakan fungsi C ++ 11 lambda

template <typename TCode, typename TFinallyCode>
inline void with_finally(const TCode &code, const TFinallyCode &finally_code)
{
    try
    {
        code();
    }
    catch (...)
    {
        try
        {
            finally_code();
        }
        catch (...) // Maybe stupid check that finally_code mustn't throw.
        {
            std::terminate();
        }
        throw;
    }
    finally_code();
}

Semoga kompiler akan mengoptimalkan kode di atas.

Sekarang kita dapat menulis kode seperti ini:

with_finally(
    [&]()
    {
        try
        {
            // Doing some stuff that may throw an exception
        }
        catch (const exception1 &)
        {
            // Handling first class of exceptions
        }
        catch (const exception2 &)
        {
            // Handling another class of exceptions
        }
        // Some classes of exceptions can be still unhandled
    },
    [&]() // finally
    {
        // This code will be executed in all three cases:
        //   1) exception was not thrown at all
        //   2) exception was handled by one of the "catch" blocks above
        //   3) exception was not handled by any of the "catch" block above
    }
);

Jika mau, Anda dapat membungkus idiom ini menjadi makro "coba - akhirnya":

// Please never throw exception below. It is needed to avoid a compilation error
// in the case when we use "begin_try ... finally" without any "catch" block.
class never_thrown_exception {};

#define begin_try    with_finally([&](){ try
#define finally      catch(never_thrown_exception){throw;} },[&]()
#define end_try      ) // sorry for "pascalish" style :(

Sekarang "akhirnya" blok tersedia di C ++ 11:

begin_try
{
    // A code that may throw
}
catch (const some_exception &)
{
    // Handling some exceptions
}
finally
{
    // A code that is always executed
}
end_try; // Sorry again for this ugly thing

Secara pribadi saya tidak suka "makro" versi "akhirnya" idiom dan lebih suka menggunakan fungsi "with_finally" murni meskipun sintaks lebih besar dalam kasus itu.

Anda dapat menguji kode di atas di sini: http://coliru.stacked-crooked.com/a/1d88f64cb27b3813

PS

Jika Anda akhirnya perlu memblokir kode Anda, maka penjaga ruang lingkup atau ON_FINALLY / ON_EXCEPTION makro mungkin akan lebih sesuai dengan kebutuhan Anda.

Berikut adalah contoh singkat penggunaan ON_FINALLY / ON_EXCEPTION:

void function(std::vector<const char*> &vector)
{
    int *arr1 = (int*)malloc(800*sizeof(int));
    if (!arr1) { throw "cannot malloc arr1"; }
    ON_FINALLY({ free(arr1); });

    int *arr2 = (int*)malloc(900*sizeof(int));
    if (!arr2) { throw "cannot malloc arr2"; }
    ON_FINALLY({ free(arr2); });

    vector.push_back("good");
    ON_EXCEPTION({ vector.pop_back(); });

    ...
anton_rh
sumber
1
Yang pertama bagi saya adalah yang paling mudah dibaca dari semua opsi yang disajikan di halaman ini. +1
Nikos
7

Maaf telah menggali utas lama seperti itu, tetapi ada kesalahan besar pada alasan berikut:

RAII memindahkan tanggung jawab keselamatan pengecualian dari pengguna objek ke perancang (dan pelaksana) objek. Saya berpendapat ini adalah tempat yang tepat karena Anda hanya perlu mendapatkan pengecualian keselamatan yang benar sekali (dalam desain / implementasi). Dengan menggunakan akhirnya Anda harus mendapatkan pengecualian keselamatan yang benar setiap kali Anda menggunakan objek.

Lebih sering daripada tidak, Anda harus berurusan dengan objek yang dialokasikan secara dinamis, jumlah objek dinamis, dll. Dalam blok percobaan, beberapa kode dapat membuat banyak objek (berapa banyak yang ditentukan saat runtime) dan menyimpan pointer ke mereka dalam daftar. Sekarang, ini bukan skenario yang eksotis, tetapi sangat umum. Dalam hal ini, Anda ingin menulis hal-hal seperti

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  finally
  {
    while (!myList.empty())
    {
      delete myList.back();
      myList.pop_back();
    }
  }
}

Tentu saja daftar itu sendiri akan dihancurkan ketika keluar dari ruang lingkup, tetapi itu tidak akan membersihkan objek sementara yang telah Anda buat.

Sebaliknya, Anda harus menempuh rute yang buruk:

void DoStuff(vector<string> input)
{
  list<Foo*> myList;

  try
  {    
    for (int i = 0; i < input.size(); ++i)
    {
      Foo* tmp = new Foo(input[i]);
      if (!tmp)
        throw;

      myList.push_back(tmp);
    }

    DoSomeStuff(myList);
  }
  catch(...)
  {
  }

  while (!myList.empty())
  {
    delete myList.back();
    myList.pop_back();
  }
}

Juga: mengapa bahasa yang dikelola bahkan menyediakan blok terakhir meskipun sumber daya secara otomatis dialokasikan oleh pengumpul sampah?

Petunjuk: masih ada lagi yang bisa Anda lakukan dengan "akhirnya" daripada hanya alokasi memori.

Mephane
sumber
17
Bahasa yang dikelola akhirnya membutuhkan pemblokiran secara tepat karena hanya satu jenis sumber daya yang dikelola secara otomatis: memori. RAII berarti bahwa semua sumber daya dapat ditangani dengan cara yang sama, jadi tidak perlu untuk akhirnya. Jika Anda benar-benar menggunakan RAII dalam contoh Anda (dengan menggunakan smart pointer di daftar Anda daripada yang telanjang), kode akan lebih sederhana daripada "akhirnya" -contoh Anda. Dan bahkan lebih sederhana jika Anda tidak memeriksa nilai pengembalian yang baru - memeriksanya tidak ada gunanya.
Myto
7
newtidak mengembalikan NULL, sebagai gantinya melempar pengecualian
Hasturkun
5
Anda mengajukan pertanyaan penting, tetapi ada 2 kemungkinan jawaban. Salah satunya adalah yang diberikan oleh Myto - gunakan smart pointer untuk semua alokasi dinamis. Yang lainnya adalah menggunakan kontainer standar, yang selalu menghancurkan isinya setelah dihancurkan. Either way, setiap objek yang dialokasikan pada akhirnya dimiliki oleh objek yang dialokasikan secara statis yang secara otomatis membebaskannya saat dihancurkan. Sangat memalukan bahwa solusi yang lebih baik ini sulit ditemukan oleh programmer karena visibilitas yang tinggi dari pointer dan array yang sederhana.
j_random_hacker
4
C ++ 11 meningkatkan ini dan termasuk std::shared_ptrdan std::unique_ptrlangsung di stdlib.
u0b34a0f6ae
16
Alasan teladan Anda begitu mengerikan bukan karena RAII cacat, melainkan karena Anda gagal menggunakannya. Pointer mentah bukan RAII.
Ben Voigt
6

FWIW, Microsoft Visual C ++ tidak mendukung coba, akhirnya dan secara historis telah digunakan dalam aplikasi MFC sebagai metode menangkap pengecualian serius yang jika tidak akan mengakibatkan crash. Sebagai contoh;

int CMyApp::Run() 
{
    __try
    {
        int i = CWinApp::Run();
        m_Exitok = MAGIC_EXIT_NO;
        return i;
    }
    __finally
    {
        if (m_Exitok != MAGIC_EXIT_NO)
            FaultHandler();
    }
}

Saya telah menggunakan ini di masa lalu untuk melakukan hal-hal seperti menyimpan cadangan file yang terbuka sebelum keluar. Pengaturan debug JIT tertentu akan merusak mekanisme ini.

SmacL
sumber
4
ingatlah bahwa itu bukan pengecualian C ++, tetapi yang SEH. Anda dapat menggunakan keduanya dalam kode MS C ++. SEH adalah handler pengecualian OS yang merupakan cara VB, .NET menerapkan pengecualian.
gbjbaanb
dan Anda dapat menggunakan SetUnhandledExceptionHandler untuk membuat penangan pengecualian yang tidak ditangkap 'global' - untuk pengecualian SEH.
gbjbaanb
3
SEH mengerikan dan juga mencegah C ++ destructor disebut
paulm
6

Seperti yang ditunjukkan dalam jawaban lain, C ++ dapat mendukung finallyfungsionalitas seperti. Implementasi fungsi ini yang mungkin paling dekat dengan menjadi bagian dari bahasa standar adalah yang menyertai Pedoman Inti C ++ , seperangkat praktik terbaik untuk menggunakan C ++ yang diedit oleh Bjarne Stoustrup dan Herb Sutter. Sebuah implementasifinally merupakan bagian dari Perpustakaan Dukungan Pedoman (GSL). Sepanjang Panduan, penggunaan finallydirekomendasikan ketika berhadapan dengan antarmuka gaya lama, dan itu juga memiliki pedoman sendiri, berjudul Gunakan objek final_action untuk mengekspresikan pembersihan jika tidak ada sumber daya pegangan yang tersedia .

Jadi, tidak hanya dukungan C ++ finally, sebenarnya disarankan untuk menggunakannya dalam banyak kasus penggunaan umum.

Contoh penggunaan implementasi GSL akan terlihat seperti:

#include <gsl/gsl_util.h>

void example()
{
    int handle = get_some_resource();
    auto handle_clean = gsl::finally([&handle] { clean_that_resource(handle); });

    // Do a lot of stuff, return early and throw exceptions.
    // clean_that_resource will always get called.
}

Implementasi dan penggunaan GSL sangat mirip dengan yang ada di jawaban Paolo.Bolzoni . Salah satu perbedaannya adalah bahwa objek yang dibuat gsl::finally()tidak memiliki disable()panggilan. Jika Anda memerlukan fungsionalitas itu (katakanlah, untuk mengembalikan sumber daya setelah dirakit dan tidak ada pengecualian yang pasti terjadi), Anda mungkin lebih memilih implementasi Paolo. Jika tidak, menggunakan GSL sedekat mungkin dengan menggunakan fitur standar seperti yang akan Anda dapatkan.

tobi_s
sumber
3

Tidak juga, tetapi Anda dapat meniru mereka sampai batas tertentu, misalnya:

int * array = new int[10000000];
try {
  // Some code that can throw exceptions
  // ...
  throw std::exception();
  // ...
} catch (...) {
  // The finally-block (if an exception is thrown)
  delete[] array;
  // re-throw the exception.
  throw; 
}
// The finally-block (if no exception was thrown)
delete[] array;

Perhatikan bahwa blok-terakhir itu sendiri mungkin melemparkan pengecualian sebelum pengecualian asli dilemparkan kembali, sehingga membuang pengecualian asli. Ini adalah perilaku yang sama persis seperti di blok akhirnya Java. Selain itu, Anda tidak dapat menggunakan returndi dalam blok coba & tangkap.

bcmpinc
sumber
3
Saya senang bahwa Anda menyebutkan blok terakhir mungkin melempar; itu adalah hal yang sebagian besar jawaban "gunakan RAII" tampaknya diabaikan. Untuk menghindari keharusan menulis blok akhirnya dua kali, Anda dapat melakukan sesuatu sepertistd::exception_ptr e; try { /*try block*/ } catch (...) { e = std::current_exception(); } /*finally block*/ if (e) std::rethrow_exception(e);
sethobrien
1
Itu saja yang ingin saya ketahui! Mengapa tidak ada jawaban lain yang menjelaskan bahwa tangkapan (...) + lemparan kosong; bekerja hampir seperti blok akhirnya? Terkadang Anda hanya membutuhkannya.
VinGarcia
Solusi yang saya berikan dalam jawaban saya ( stackoverflow.com/a/38701485/566849 ) harus memungkinkan untuk melempar pengecualian dari dalam finallyblok.
Fabio A.
3

Aku datang dengan finallymakro yang dapat digunakan hampir seperti ¹ yang finallykata kunci di Jawa; itu menggunakan std::exception_ptrdan teman-teman, fungsi lambda dan std::promise, sehingga membutuhkan C++11atau di atas; itu juga menggunakan pernyataan ekspresi ekstensi GCC, yang juga didukung oleh dentang.

PERINGATAN : versi sebelumnya dari jawaban ini menggunakan implementasi konsep yang berbeda dengan lebih banyak keterbatasan.

Pertama, mari kita mendefinisikan kelas pembantu.

#include <future>

template <typename Fun>
class FinallyHelper {
    template <typename T> struct TypeWrapper {};
    using Return = typename std::result_of<Fun()>::type;

public:    
    FinallyHelper(Fun body) {
        try {
            execute(TypeWrapper<Return>(), body);
        }
        catch(...) {
            m_promise.set_exception(std::current_exception());
        }
    }

    Return get() {
        return m_promise.get_future().get();
    }

private:
    template <typename T>
    void execute(T, Fun body) {
        m_promise.set_value(body());
    }

    void execute(TypeWrapper<void>, Fun body) {
        body();
    }

    std::promise<Return> m_promise;
};

template <typename Fun>
FinallyHelper<Fun> make_finally_helper(Fun body) {
    return FinallyHelper<Fun>(body);
}

Lalu ada makro yang sebenarnya.

#define try_with_finally for(auto __finally_helper = make_finally_helper([&] { try 
#define finally });                         \
        true;                               \
        ({return __finally_helper.get();})) \
/***/

Dapat digunakan seperti ini:

void test() {
    try_with_finally {
        raise_exception();
    }    

    catch(const my_exception1&) {
        /*...*/
    }

    catch(const my_exception2&) {
        /*...*/
    }

    finally {
        clean_it_all_up();
    }    
}

Penggunaan std::promisemembuatnya sangat mudah untuk diimplementasikan, tetapi mungkin juga memperkenalkan sedikit overhead yang tidak dibutuhkan yang dapat dihindari dengan menerapkan kembali hanya fungsi yang diperlukan saja std::promise.


¹ CAVEAT: ada beberapa hal yang tidak berfungsi seperti versi java finally. Dari atas kepala saya:

  1. tidak mungkin untuk memutuskan dari loop luar dengan breakpernyataan dari dalam trydancatch() 's blok, karena mereka hidup dalam fungsi lambda;
  2. harus ada setidaknya satu catch()blok setelahtry : itu persyaratan C ++;
  3. jika fungsi memiliki nilai kembali selain void tetapi tidak ada pengembalian di dalam trydan catch()'sblok, kompilasi akan gagal karena finallymakro akan diperluas ke kode yang ingin mengembalikan a void. Ini bisa, err, dibatalkan dengan memiliki semacam finally_noreturnmakro.

Semua dalam semua, saya tidak tahu apakah saya akan pernah menggunakan barang ini sendiri, tapi itu menyenangkan bermain dengannya. :)

Fabio A.
sumber
Ya, itu hanya peretasan cepat, tetapi jika programmer tahu apa yang mereka lakukan mungkin akan berguna.
Fabio A.
@ MarkLakata, saya memperbarui posting dengan implementasi yang lebih baik yang mendukung melempar pengecualian dan pengembalian.
Fabio A.
Kelihatan bagus. Anda bisa menyingkirkan Caveat 2 dengan hanya menempatkan di catch(xxx) {}blok yang mustahil di awal finallymakro, di mana xxx adalah tipe palsu hanya untuk keperluan memiliki setidaknya satu blok tangkapan.
Mark Lakata
@MarkLakata, saya juga memikirkan itu, tapi itu tidak memungkinkan untuk digunakan catch(...), bukan?
Fabio A.
Saya kira tidak. Buat saja jenis yang tidak dikenal xxxdalam ruang nama pribadi yang tidak akan pernah digunakan.
Mark Lakata
2

Saya memiliki kasus penggunaan di mana saya pikir finally harus menjadi bagian yang dapat diterima dari bahasa C ++ 11, karena saya pikir lebih mudah untuk membaca dari sudut pandang aliran. Case use saya adalah rantai utas konsumen / produsen, di mana sentinel nullptrdikirim pada akhir proses untuk mematikan semua utas.

Jika C ++ mendukungnya, Anda ingin kode Anda terlihat seperti ini:

    extern Queue downstream, upstream;

    int Example()
    {
        try
        {
           while(!ExitRequested())
           {
             X* x = upstream.pop();
             if (!x) break;
             x->doSomething();
             downstream.push(x);
           } 
        }
        finally { 
            downstream.push(nullptr);
        }
    }

Saya pikir ini lebih logis bahwa menempatkan akhirnya deklarasi Anda di awal loop, karena itu terjadi setelah loop telah keluar ... tapi itu angan-angan karena kita tidak bisa melakukannya di C ++. Perhatikan bahwa antrian downstreamterhubung ke utas lain, sehingga Anda tidak dapat memasukkan sentinel ke push(nullptr)dalam destruktor downstreamkarena tidak dapat dihancurkan pada saat ini ... ia harus tetap hidup sampai utas lainnya menerimanullptr .

Jadi, inilah cara menggunakan kelas RAII dengan lambda untuk melakukan hal yang sama:

    class Finally
    {
    public:

        Finally(std::function<void(void)> callback) : callback_(callback)
        {
        }
        ~Finally()
        {
            callback_();
        }
        std::function<void(void)> callback_;
    };

dan inilah cara Anda menggunakannya:

    extern Queue downstream, upstream;

    int Example()
    {
        Finally atEnd([](){ 
           downstream.push(nullptr);
        });
        while(!ExitRequested())
        {
           X* x = upstream.pop();
           if (!x) break;
           x->doSomething();
           downstream.push(x);
        }
    }
Mark Lakata
sumber
Hai, saya yakin jawaban saya di atas ( stackoverflow.com/a/38701485/566849 ) sepenuhnya memenuhi persyaratan Anda.
Fabio A.
1

Seperti yang banyak orang nyatakan, solusinya adalah menggunakan fitur C ++ 11 untuk menghindari blok yang terakhir. Salah satu fiturnya adalah unique_ptr.

Inilah jawaban Mephane yang ditulis menggunakan pola RAII.

#include <vector>
#include <memory>
#include <list>
using namespace std;

class Foo
{
 ...
};

void DoStuff(vector<string> input)
{
    list<unique_ptr<Foo> > myList;

    for (int i = 0; i < input.size(); ++i)
    {
      myList.push_back(unique_ptr<Foo>(new Foo(input[i])));
    }

    DoSomeStuff(myList);
}

Beberapa pengantar untuk menggunakan unique_ptr dengan wadah C ++ Standard Library ada di sini

Mark Lakata
sumber
0

Saya ingin memberikan alternatif.

Jika Anda ingin akhirnya blok dipanggil selalu, cukup letakkan setelah block catch terakhir (yang mungkin seharusnya catch( ... )untuk menangkap pengecualian tidak dikenal)

try{
   // something that might throw exception
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called always,
//don't forget that it might throw some exception too
doSomeCleanUp(); 

Jika Anda ingin akhirnya memblokir sebagai hal terakhir yang harus dilakukan ketika ada pengecualian yang dilemparkan, Anda dapat menggunakan variabel lokal boolean - sebelum menjalankan Anda mengaturnya ke false dan menempatkan tugas yang benar di akhir blok coba, kemudian setelah menangkap blok periksa untuk variabel nilai:

bool generalAppState = false;
try{
   // something that might throw exception

   //the very end of try block:
   generalAppState = true;
} catch( ... ){
   // what to do with uknown exception
}

//final code to be called only when exception was thrown,
//don't forget that it might throw some exception too
if( !generalAppState ){
   doSomeCleanUpOfDirtyEnd();
}

//final code to be called only when no exception is thrown
//don't forget that it might throw some exception too
else{
   cleanEnd();
}
jave.web
sumber
Ini tidak berhasil, karena seluruh titik blok akhirnya adalah untuk melakukan pembersihan bahkan ketika kode harus memungkinkan pengecualian untuk meninggalkan blok kode. Pertimbangkan: `coba {// hal-hal yang mungkin melempar" B "} tangkapan (A & a) {} akhirnya {// jika C ++ memilikinya ... // hal-hal yang harus terjadi, bahkan jika" B "dilemparkan. } // tidak akan mengeksekusi jika "B" dilemparkan. `IMHO, titik pengecualian adalah untuk mengurangi kode penanganan kesalahan, jadi tangkap blok, di mana pun terjadi lemparan, kontraproduktif. Inilah mengapa RAII membantu: jika diterapkan secara bebas, pengecualian paling penting di lapisan atas dan bawah.
burlyearly
1
@burlyearly meskipun pendapat Anda tidak suci, saya mengerti, tetapi dalam C ++ tidak ada hal seperti itu sehingga Anda harus menganggap ini sebagai lapisan teratas yang meniru perilaku ini.
jave.web
DOWNVOTE = TOLONG KOMENTAR :)
jave.web
0

Saya juga berpikir bahwa RIIA bukan pengganti yang sepenuhnya berguna untuk penanganan pengecualian dan akhirnya. BTW, saya juga berpikir RIIA adalah nama yang buruk di sekitar. Saya menyebut jenis kelas ini sebagai 'petugas kebersihan' dan menggunakannya sebagai BANYAK. 95% dari waktu mereka tidak menginisialisasi atau memperoleh sumber daya, mereka menerapkan beberapa perubahan berdasarkan cakupan, atau mengambil sesuatu yang sudah disiapkan dan memastikan itu dihancurkan. Ini menjadi internet resmi yang terobsesi dengan nama pola yang saya gunakan untuk bahkan menyarankan nama saya mungkin lebih baik.

Saya hanya berpikir itu tidak masuk akal untuk mengharuskan bahwa setiap pengaturan rumit dari beberapa hal ad hoc harus memiliki kelas tertulis untuk menampungnya untuk menghindari komplikasi ketika membersihkan semuanya kembali dalam menghadapi kebutuhan untuk menangkap beberapa jenis pengecualian jika terjadi kesalahan dalam proses. Ini akan menyebabkan banyak kelas ad hoc yang tidak diperlukan jika tidak sebaliknya.

Ya itu baik untuk kelas yang dirancang untuk mengelola sumber daya tertentu, atau yang generik yang dirancang untuk menangani serangkaian sumber daya yang serupa. Tetapi, bahkan jika semua hal yang terlibat memiliki pembungkus seperti itu, koordinasi pembersihan mungkin tidak hanya sederhana dalam permintaan terbalik dari para penghancur.

Saya pikir itu masuk akal untuk C ++ untuk akhirnya. Maksudku, ya ampun, begitu banyak bit dan bobs telah terpaku padanya selama beberapa dekade terakhir sehingga tampaknya orang-orang aneh tiba-tiba menjadi konservatif atas sesuatu seperti akhirnya yang bisa sangat berguna dan mungkin tidak ada yang rumit seperti beberapa hal lain yang telah menambahkan (meskipun itu hanya dugaan di pihak saya.)

Dean Roddey
sumber
-2
try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}
Pengecualian yang tidak tertangani
sumber
35
Idiom lucu, tetapi tidak persis sama. kembali di blok coba atau tangkap tidak akan melewati kode 'akhirnya:' Anda.
Edward KMETT
10
Sebaiknya simpan jawaban yang salah ini (dengan nilai 0), karena Edward Kmett mengemukakan perbedaan yang sangat penting.
Mark Lakata
12
Bahkan cacat yang lebih besar (IMO): Kode ini memakan semua pengecualian, yang finallytidak dilakukan.
Ben Voigt