C ++ 'nihayet' blokları destekliyor mu? (Peki duymaya devam ettiğim bu 'RAII' nedir?)


Yanıtlar:


273

Hayır, C ++ 'nihayet' blokları desteklemez. Bunun nedeni, C ++'ın RAII'yi desteklemesidir: "Kaynak Edinimi Başlatmadır" - gerçekten kullanışlı bir kavram için kötü bir isim .

Fikir, bir nesnenin yıkıcısının kaynakları boşaltmaktan sorumlu olmasıdır. Nesne otomatik depolama süresine sahip olduğunda, nesnenin yıkıcısı, oluşturulduğu blok çıktığında (bu blok bir istisna varlığında çıkılsa bile) çağrılır. İşte Bjarne Stroustrup'un konuyla ilgili açıklaması .

RAII için yaygın bir kullanım, bir muteksi kilitlemektir:

// 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 ayrıca nesneleri diğer sınıfların üyeleri olarak kullanmayı da kolaylaştırır. Sahip olan sınıf 'yok edildiğinde, RAII sınıfı tarafından yönetilen kaynak serbest bırakılır, çünkü RAII tarafından yönetilen sınıfın yıkıcısı sonuç olarak çağrılır. Bu, kaynakları yöneten bir sınıftaki tüm üyeler için RAII kullandığınızda, üye kaynak yaşamlarını manuel olarak yönetmesi gerekmediği için sahip sınıfı için çok basit, belki de varsayılan bir yıkıcıyı kullanarak kurtulabilirsiniz. . ( Bunu işaret ettiği için Mike B'ye teşekkürler .)

C # veya VB.NET içeren familliarlar için, RAII'nin IDisposable ve 'using' ifadelerini kullanarak .NET deterministik yıkımına benzer olduğunu fark edebilirsiniz . Aslında, iki yöntem birbirine çok benzer. Temel fark, RAII'nin bellek dahil olmak üzere her türlü kaynağı kararlı bir şekilde serbest bırakmasıdır. .NET'te IDisposable (.NET dili C ++ / CLI bile) uygulandığında, bellek dışında kaynaklar deterministik olarak serbest bırakılır. .NET'te, bellek deterministik olarak serbest bırakılmaz; bellek yalnızca çöp toplama döngüleri sırasında serbest bırakılır.

 

† Bazı insanlar "Yıkımın Kaynaktan Ayrılması" nın RAII deyimi için daha doğru bir isim olduğuna inanmaktadır.


18
"Yıkım Kaynaktan Ayrılmadır" - DIRR ... Hayır, benim için çalışmıyor. = P
Erik Forbes

14
RAII sıkışmış - gerçekten değişmiyor. Bunu yapmaya çalışmak aptalca olur. Ancak, "Kaynak Edinimi Başlatmadır" ın hala oldukça zayıf bir isim olduğunu kabul etmelisiniz.
Kevin

162
SBRM == Kapsam Bağlı Kaynak Yönetimi
Johannes Schaub - litb

10
Geliştirilmiş teknikleri bir yana, sadece yazılım değil, genel olarak mühendislik yapma becerisine sahip olan herkes, böyle korkunç bir kısaltma için layık bir bahane veremez.
Hardryv

54
Bu, herhangi bir C ++ nesnesinin ömrüne uymayan bir şey temizlediğinizde takılı kalmanızı sağlar. Sanırım ömür boyu eşittir C + + sınıf Liftime veya Else It Çirkin alır (LECCLEOEIGU?).
Warren P

79

C ++ 'da nihayet RAII nedeniyle gerekli DEĞİLDİR .

RAII, istisna güvenliği sorumluluğunu nesnenin kullanıcısından nesnenin tasarımcısına (ve uygulayıcısına) taşır. O zaman sadece bir kez (tasarım / uygulamada) istisna güvenliği doğru almak gerekir gibi bu doğru yer olduğunu iddia ediyorum. Son olarak, bir nesneyi her kullandığınızda istisna güvenliğini düzeltmeniz gerekir.

Ayrıca IMO kodu daha temiz görünüyor (aşağıya bakınız).

Misal:

Bir veritabanı nesnesi. DB bağlantısının kullanıldığından emin olmak için açılmalı ve kapatılmalıdır. RAII kullanarak bu yapıcı / yıkıcı içinde yapılabilir.

C ++ Benzeri 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.

RAII kullanımı, bir DB nesnesinin doğru kullanımını çok kolaylaştırır. DB nesnesi, onu nasıl denediğimiz ve kötüye kullandığımız önemli değil, bir yıkıcı kullanarak kendini doğru şekilde kapatacaktır.

Sonunda Java Gibi

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.
        }
    }
}

Son olarak kullanıldığında, nesnenin doğru kullanımı, nesnenin kullanıcısına devredilir. yani DB bağlantısını açıkça kapatmak nesne kullanıcısının sorumluluğundadır. Şimdi bunun sonlandırıcıda yapılabileceğini iddia edebilirsiniz, ancak kaynakların sınırlı kullanılabilirliği veya diğer kısıtlamaları olabilir ve bu nedenle genellikle nesnenin serbest bırakılmasını kontrol etmek ve çöp toplayıcının deterministik olmayan davranışına güvenmek istemezsiniz.

Ayrıca bu basit bir örnek.
Serbest bırakılması gereken birden fazla kaynağınız olduğunda kod karmaşıklaşabilir.

Daha ayrıntılı bir analiz burada bulunabilir: http://accu.org/index.php/journals/236


16
// Make sure not to throw exception if one is already propagating.C ++ yıkıcılarının bu nedenle istisnalar atmaması önemlidir.
21'de Cema için

10
@Cemafor: C ++ 'ın yıkıcıdan istisna atmamasının nedeni Java'dan farklıdır. Java'da çalışır (sadece orijinal istisnayı kaybedersiniz). C ++ 'da gerçekten kötü. Ancak C ++ 'daki nokta, yıkıcıyı yazdığında sadece bir kez (sınıfın tasarımcısı tarafından) yapmanız gerektiğidir. Java'da bunu kullanım noktasında yapmanız gerekir. Bu nedenle, aynı kazan plakasını çok zaman yazmak, sınıf kullanıcısının sorumluluğundadır.
Martin York

1
"Gerekli" olma meselesi varsa, RAII'ye de ihtiyacınız yoktur. Hadi kurtuyalım! :-) Şaka bir yana, RAII birçok durum için iyidir. RAII'nin yaptığı daha hantal bir şey, yukarıdaki kod erken dönse bile bazı kodları (kaynakla ilgili değil) yürütmek istediğiniz durumlardır. Bunun için ya gotos kullanırsınız ya da iki metoda ayırırsınız.
Trinidad

1
@Trinidad: Düşündüğünüz gibi basit değil (tüm önerileriniz mümkün olan en kötü seçenekleri seçiyor gibi görünüyor). Bu yüzden bir soru, bunu keşfetmek için yorumlardan daha iyi bir yer olabilir.
Martin York

1
"RAII nedeniyle gerekli değildir" eleştirisi: ad-hoc RAII eklemenin eklemek için çok fazla kaynak kodu ve denemenin son derece uygun olacağı birçok durum vardır.
ceztko

63

RAII genellikle daha iyidir, ancak C ++ ' da nihayet anlambilime kolayca sahip olabilirsiniz . Az miktarda kod kullanma.

Ayrıca, C ++ Temel Yönergeleri nihayet verir.

İşte bağlantısıdır GSL Microsoft uygulanması için ve bir bağlantı Martin Moene uygulanması

Bjarne Stroustrup birçok kez GSL'de olan her şeyin sonunda standartta olması gerektiğini söyledi. Bu yüzden sonunda kullanmak için geleceğe dönük bir yol olmalı .

İsterseniz kolayca uygulayabilirsiniz, okumaya devam edin.

C ++ 11 RAII ve lambdas, nihayet bir genel yapmaya izin verir:

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); }

kullanım örneği:

#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"; }

çıktı:

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

Şahsen bu birkaç kez bir C ++ programında POSIX dosya tanımlayıcıyı kapatmak için kullandım.

Kaynakları yöneten ve böylece her türlü sızıntıyı önleyen gerçek bir sınıfa sahip olmak genellikle daha iyidir, ancak bu nihayet bir sınıfın aşırıya kaçmasına benzediği durumlarda yararlıdır.

Ayrıca, nihayet diğer dillerden daha çok hoşuma gidiyor çünkü doğal olarak kullanılırsa kapanış kodunu açılış kodunun yakınına yazıyorsunuz (benim örneğimde yeni ve sil ) ve imha, C ++ 'da her zamanki gibi LIFO düzeninde inşaatı takip ediyor. Tek dezavantajı, gerçekten kullanmadığınız bir otomatik değişken elde etmeniz ve lambda sözdizimini biraz gürültülü hale getirmesidir (örneğimde dördüncü satırda sadece son olarak kelime ve sağdaki {} -block anlamlı, dinlenme aslında gürültü).

Başka bir örnek:

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

Devre dışı bırakma üyesi, yalnızca başarısızlık durumunda son aranması gerektiğinde yararlıdır . Örneğin, bir nesneyi üç farklı kapsayıcıya kopyalamanız gerekir, son olarak her kopyayı geri alacak ve tüm kopyalar başarılı olduktan sonra devre dışı bırakacak şekilde ayarlayabilirsiniz . Bunu yaparsanız, yıkım atılamazsa, güçlü garantiyi garanti edersiniz.

örneği devre dışı bırak :

//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(); }

C ++ 11 kullanamıyorsanız, son olarak sahip olabilirsiniz , ancak kod biraz daha uzun sarılır. Sadece bir yapıcı ve yıkıcı ile bir yapı tanımlayın, yapıcı ihtiyaç duyduğunuz her şeye referanslar alır ve yıkıcı ihtiyacınız olan eylemleri yapar. Temel olarak lambda'nın yaptığı budur.

#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"; }

Olası bir sorun olabilir: 'nihayet (F f)' fonksiyonunda bir FinalAction nesnesini döndürür, bu nedenle nihayetinde işlevin geri dönmesinden önce yapısökücü çağrılabilir. Belki şablon F yerine std :: fonksiyonunu kullanmalıyız.
user1633272

FinalActionTemel olarak popüler ScopeGuarddeyimle aynı olduğunu unutmayın , sadece farklı bir adla.
anderas

1
Bu optimizasyon güvenli mi?
Nulano

2
@ Paolo.Bolzoni Cevap vermediğim için üzgünüm, yorumunuz için bildirim almadım. Ben (hangi bir DLL fonksiyonu çağırmak) sonunda blok (değişken kullanılmayan çünkü) kapsamının sonuna önce çağrılacak endişe vardı, ama o zamandan beri benim endişelerimi temizledi SO bir soru bulduk. Buna bağlanırdım, ama maalesef artık bulamıyorum.
Nulano

1
Enable () işlevi, aksi takdirde temiz tasarımınızdaki bir siğildir. Sonunda sadece başarısızlık durumunda çağrılmasını istiyorsanız, neden sadece catch deyimini kullanmıyorsunuz? Bunun için değil mi?
user2445507

32

Yığın tabanlı nesnelerle temizlemeyi kolaylaştırmanın ötesinde, RAII de yararlıdır, çünkü nesne 'sınıfın bir üyesi olduğunda aynı' otomatik 'temizleme gerçekleşir. Sahip olan sınıf yok edildiğinde, RAII sınıfı tarafından yönetilen kaynak temizlenir, çünkü bu sınıfın dtor'u sonuç olarak çağrılır.

Bu, RAII nirvana'ya ulaştığınızda ve bir sınıftaki tüm üyeler RAII (akıllı işaretçiler gibi) kullandığında, sahibinin sınıfı için çok basit (belki de varsayılan) bir dtor ile manuel olarak yönetilmesi gerekmediği anlamına gelir. üye kaynak ömürleri.


Bu çok iyi bir nokta. Size + 1'leyin. Yine de pek çok kişi size oy vermedi. Umarım yorumunuzu içerecek şekilde yayınımı düzenlediğim için sakıncası yoktur. (Sana elbette kredi verdim.) Teşekkürler! :)
Kevin

30

neden çöp toplayıcı tarafından kaynakların otomatik olarak dağıtılmasına rağmen yönetilen diller bile nihayet bir engelleme sağlıyor?

Aslında, Çöp toplayıcılarına dayalı diller "nihayet" daha fazlasına ihtiyaç duyar. Bir çöp toplayıcı nesnelerinizi zamanında yok etmez, bu nedenle bellekle ilgili olmayan sorunları doğru bir şekilde temizlemek için güvenilemez.

Dinamik olarak ayrılmış veriler açısından, çoğu kişi akıllı işaretçiler kullanmanız gerektiğini savunur.

Ancak...

RAII, istisna güvenliği sorumluluğunu nesnenin kullanıcısından tasarımcıya taşır

Ne yazık ki bu kendi çöküşü. Eski C programlama alışkanlıkları çok ölüyor. C veya çok C stilinde yazılmış bir kitaplık kullandığınızda, RAII kullanılmaz. Tüm API ön ucunu yeniden yazmadan kısa bir süre sonra, bununla çalışmak zorundasınız. Sonra "nihayet" eksikliği gerçekten ısırır.


13
Aynen ... RAII ideal bir perspektiften hoş görünüyor. Ama her zaman geleneksel C API'leri ile çalışmak zorunda (Win32 API C tarzı fonksiyonları gibi ...). Temizlemek için CloseHandle (HANDLE) gibi bir işlev gerektiren bir çeşit HANDLE döndüren bir kaynak elde etmek çok yaygındır. Try ... kullanmak nihayet olası istisnalarla başa çıkmanın güzel bir yoludur. (Neyse ki, özel silicilerle paylaşılan_ptr gibi görünüyor ve C ++ 11 lambdas, yalnızca tek bir yerde kullandığım bazı API'ları sarmak için tüm sınıfları yazmayı gerektirmeyen bazı RAII tabanlı rahatlama sağlamalıdır.).
James Johnston

7
@JamesJohnston, her türlü tutamağı tutan ve RAII mekaniği sağlayan bir sarıcı sınıfı yazmak çok kolaydır. ATL, örneğin bir demet sağlar. Görünüşe göre bunu çok fazla sorun olarak görüyorsunuz ama katılmıyorum, çok küçük ve yazması kolay.
Mark Ransom

5
Basit evet, küçük hayır. Boyut, üzerinde çalıştığınız kütüphanenin karmaşıklığına bağlıdır.
Philip Couling

1
@MarkRansom: Başka bir istisna beklemedeyken temizleme sırasında bir istisna oluşursa RAII'nin akıllı bir şey yapabileceği herhangi bir mekanizma var mı? Deneme / son olarak kullanılan sistemlerde - garip olsa da - temizleme sırasında oluşan istisna ve istisnanın her ikisinin de yeni bir yerde saklanması için bir şeyler düzenlemek mümkündür CleanupFailedException. RAII kullanarak böyle bir sonuç elde etmenin makul bir yolu var mı?
supercat

3
@couling: Bir programın bir SomeObject.DoSomething()yöntemi çağıracağı ve (1) başarılı, (2) yan etkisi olmadan başarısız olduğu , (3) arayanın başa çıkmaya hazır olduğu yan etkilerle başarısız olup olmadığını bilmek istediği birçok durum vardır. veya (4) arayanın baş edemediği yan etkilerle başarısız oldu. Sadece arayan, hangi durumlarla başa çıkabileceğini ve başa çıkamayacağını bilecektir; Arayanın ihtiyacı olan şey, durumun ne olduğunu bilmenin bir yoludur. Bir istisna hakkında en önemli bilgileri sağlamak için standart bir mekanizma olmaması çok kötü.
supercat

9

C ++ 11 lambda fonksiyonlarını kullanan bir başka "son olarak" blok emülasyonu

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();
}

Umarım derleyici yukarıdaki kodu optimize eder.

Şimdi böyle bir kod yazabiliriz:

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
    }
);

İsterseniz bu deyimi "try - nihayet" makrolarına sarabilirsiniz:

// 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 :(

Şimdi "nihayet" blok C ++ 11'de kullanılabilir:

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

Şahsen ben "nihayet" deyimin "makro" sürümünü sevmiyorum ve bu durumda sözdizimi daha hantal olsa bile saf "with_finally" fonksiyonu kullanmayı tercih ederim.

Yukarıdaki kodu buradan test edebilirsiniz: http://coliru.stacked-crooked.com/a/1d88f64cb27b3813

PS

Kodunuzda nihayet bir bloğa ihtiyacınız varsa , kapsamlı korumalar veya ON_FINALLY / ON_EXCEPTION makroları muhtemelen ihtiyaçlarınıza daha iyi uyacaktır.

İşte ON_FINALLY / ON_EXCEPTION kısa kullanım örneği:

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(); });

    ...

1
Birincisi bana bu sayfada sunulan tüm seçeneklerden en okunabilir olanı. +1
Nikos

7

Böyle eski bir iş parçacığını kazdığımız için özür dilerim, ancak aşağıdaki mantıkta büyük bir hata var:

RAII, istisna güvenliği sorumluluğunu nesnenin kullanıcısından nesnenin tasarımcısına (ve uygulayıcısına) taşır. O zaman sadece bir kez (tasarım / uygulamada) istisna güvenliği doğru almak gerekir gibi bu doğru yer olduğunu iddia ediyorum. Son olarak, bir nesneyi her kullandığınızda istisna güvenliğini düzeltmeniz gerekir.

Daha sık olmamakla birlikte, dinamik olarak tahsis edilmiş nesneler, dinamik nesne sayıları vb. Şimdi, bu egzotik bir senaryo değil, çok yaygın. Bu durumda, aşağıdaki gibi şeyler yazmak istersiniz:

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();
    }
  }
}

Tabii ki kapsam dışına çıkarken listenin kendisi yok edilecek, ancak bu oluşturduğunuz geçici nesneleri temizlemeyecektir.

Bunun yerine, çirkin rotaya gitmelisiniz:

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();
  }
}

Ayrıca: yönetilen kanallar bile, çöp toplayıcı tarafından otomatik olarak yeniden yerleştirilen kaynaklara rağmen nihayet bir engelleme sağlıyor?

İpucu: "nihayet" ile yapabileceğiniz daha çok bellek ayırmadan daha fazlasını yapabilirsiniz.


17
Yönetilen dillerin son olarak kesin olarak engellenmesi gerekir, çünkü yalnızca bir tür kaynak otomatik olarak yönetilir: bellek. RAII, tüm kaynakların aynı şekilde ele alınabileceği anlamına gelir, bu yüzden nihayet gerekli değildir. Örneğinizde gerçekten RAII kullandıysanız (listenizde çıplak olanlar yerine akıllı işaretçiler kullanarak), kod "nihayet" örneğinizden daha basit olacaktır. Ve yeni dönüş değerini kontrol etmezseniz daha da basittir - kontrol etmek neredeyse anlamsızdır.
Myto

7
newNULL döndürmez, bunun yerine bir istisna atar
Hasturkun

5
Önemli bir soru soruyorsunuz, ancak 2 olası cevabı var. Birincisi, Myto - tarafından verilen tüm dinamik ayırmalar için akıllı işaretçilerdir. Diğeri, imha üzerine her zaman içeriğini yok eden standart kaplar kullanmaktır. Her iki durumda da, tahsis edilen her nesnenin sonucu olarak imha üzerine otomatik olarak serbest bırakılan statik olarak tahsis edilen bir nesne aittir. Bu daha iyi çözümlerin, düz işaretçilerin ve dizilerin yüksek görünürlüğü nedeniyle programcılar için keşfedilmesi zor bir utanç.
j_random_hacker

4
C ++ 11 bunu geliştirir std::shared_ptrve std::unique_ptrdoğrudan stdlib'i içerir.
u0b34a0f6ae

16
Örneğinizin bu kadar korkunç görünmesinin nedeni, RAII'nin kusurlu olması değil, onu kullanamamanızdır. Ham işaretçiler RAII değildir.
Ben Voigt

6

FWIW, Microsoft Visual C ++, nihayet denemeyi destekliyor ve tarihsel olarak MFC uygulamalarında, aksi takdirde bir çökmeye neden olacak ciddi istisnaları yakalama yöntemi olarak kullanıldı. Örneğin;

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

Bunu geçmişte çıkıştan önce açık dosyaların yedeklerini kaydetmek gibi şeyler yapmak için kullandım. Ancak bazı JIT hata ayıklama ayarları bu mekanizmayı bozacaktır.


4
unutmayın ki bu C ++ istisnaları değil, SEH olanlar. Her ikisini de MS C ++ kodunda kullanabilirsiniz. SEH, VB, .NET'in istisnaları uygulama şekli olan bir OS istisna işleyicisidir.
gbjbaanb

ve SEH istisnaları için 'global' yakalanmamış özel durum işleyici oluşturmak için SetUnhandledExceptionHandler'ı kullanabilirsiniz.
gbjbaanb

3
SEH korkunçtur ve ayrıca C ++ yıkıcılarının çağrılmasını önler
paulm

6

Diğer cevaplarda belirtildiği gibi, C ++ finallybenzeri işlevselliği destekleyebilir . Muhtemelen standart dilin bir parçası olmaya en yakın olan bu işlevselliğin uygulanması, Bjarne Stoustrup ve Herb Sutter tarafından düzenlenen C ++ kullanımı için en iyi uygulamalar olan C ++ Temel Yönergeleri'ne eşlik eden işlevdir. Uygulamasının uygulanmasıfinally , Kılavuzlar Destek Kitaplığı'nın (GSL) bir parçasıdır . Yönergeler boyunca, finallyeski stil arabirimleriyle uğraşırken bunun kullanılması önerilir ve ayrıca uygun bir kaynak tanıtıcısı yoksa temizlemeyi ifade etmek için bir final_action nesnesi kullanma başlıklı kendi kılavuzuna sahiptir .

Bu nedenle, sadece C ++ desteklemekle kalmaz finally, birçok yaygın kullanım durumunda da kullanılması önerilir.

GSL uygulamasının örnek kullanımı şöyle görünecektir:

#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.
}

GSL uygulaması ve kullanımı Paolo.Bolzoni'nin cevabındaki ile çok benzer . Farklardan biri, tarafından oluşturulan nesnenin çağrıdan gsl::finally()yoksun olmasıdır disable(). Bu işlevselliğe ihtiyacınız varsa (örneğin, birleştirildikten sonra kaynağı döndürmek ve istisnalar olmayacak şekilde), Paolo'nun uygulamasını tercih edebilirsiniz. Aksi takdirde, GSL kullanmak, alacağınız standart özellikleri kullanmaya yakındır.


3

Gerçekten değil, ancak onları bir dereceye kadar taklit edebilirsiniz, örneğin:

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;

Nihayet bloğun, orijinal istisna atılmadan önce bir istisna atabileceğini ve böylece orijinal istisnayı atabileceğini unutmayın. Bu, bir Java nihayet bloğundakiyle aynı davranıştır. Ayrıca, returntry & catch bloklarının içinde kullanamazsınız .


3
Nihayet bloğun atılabileceğinden bahsettiğiniz için memnunum; "RAII kullan" yanıtlarının çoğunun göz ardı ettiği bir şey. Sonunda bloğu iki kez yazmak zorunda kalmamak için şöyle bir şey yapabilirsinizstd::exception_ptr e; try { /*try block*/ } catch (...) { e = std::current_exception(); } /*finally block*/ if (e) std::rethrow_exception(e);
sethobrien

1
Tüm bilmek istediğim bu! Neden diğer cevapların hiçbiri bir yakalama (...) + boş atış olduğunu açıkladı; neredeyse sonunda bir blok gibi çalışır? Bazen ihtiyacın var.
VinGarcia

Cevabımda verdiğim çözüm ( stackoverflow.com/a/38701485/566849 ) finallybloğun içinden istisnalar atmaya izin vermelidir .
Fabio A.

3

Ben ile geldi finallykullanılabilecek makro neredeyse gibi ¹ finallyJava anahtar kelime; std::exception_ptrve arkadaşları, lambda fonksiyonlarını kullanır ve std::promisebu nedenle C++11veya daha fazlasını gerektirir ; ayrıca clang tarafından da desteklenen bileşik ifade ifadesi GCC uzantısını kullanır.

UYARI : Bu cevabın önceki bir versiyonunda , kavramın çok daha fazla sınırlaması olan farklı bir uygulaması kullanılmıştır.

İlk olarak bir yardımcı sınıf tanımlayalım.

#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);
}

Bir de asıl makro var.

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

Bu şekilde kullanılabilir:

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

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

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

    finally {
        clean_it_all_up();
    }    
}

Uygulamanın kullanımı std::promiseçok kolaylaşır, ancak muhtemelen sadece gerekli işlevselliklerin yeniden uygulanmasıyla önlenebilecek biraz fazla gereksiz yükü de beraberinde getirir std::promise.


¹ CAVEAT: Java sürümü gibi çalışmayan birkaç şey var finally. Kafamın üstünden:

  1. onunla bir dış döngü kırmak mümkün değildir breakiçinden açıklamadatrycatch()lambda işlevi içinde yaşadıkları için, bloktan ve bloklarının ;
  2. en az bir tane olmalı catch()sonra bloktry : bu bir C ++ gereksinimi;
  3. işlevin void dışında bir dönüş değeri varsa ancak tryve catch()'sblokları içinde bir dönüş yoksa , finallymakro döndürmek isteyecek koda genişleyeceği için derleme başarısız olur void. Bu, bir çeşit makroya sahip olarak geçersiz sayılabilir finally_noreturn.

Sonuçta, bu şeyleri kendim kullanıp kullanamayacağımı bilmiyorum, ama onunla oynamak eğlenceliydi. :)


Evet, bu sadece hızlı bir hackti, ancak programcı ne yaptığını biliyorsa yine de yararlı olabilir.
Fabio A.

@MarkLakata, gönderimi istisnalar ve iadeler atmayı destekleyen daha iyi bir uygulama ile güncelledim.
Fabio A.

İyi görünüyor. Caveat 2'den sadece makronun catch(xxx) {}başlangıcında imkansız bir blok koyarak kurtulabilirsiniz finally, burada xxx sadece en az bir catch bloğuna sahip olmak için sahte bir türdür.
Mark Lakata

@ MarkLakata, bunu ben de düşündüm, ama bu kullanımı imkansız hale catch(...)getirecek değil mi?
Fabio A.

Ben öyle düşünmüyorum. xxxAsla kullanılmayacak özel bir ad alanında belirsiz bir tür oluşturun .
Mark Lakata

2

Bir akış açısından okumak daha kolay olduğunu düşündüğüm için C ++ 11 dilinin mükemmel kabul edilebilir bir parçası finally olması gerektiğini düşündüğüm bir kullanım durumum var . Kullanım durumum, nullptrtüm iş parçacıklarını kapatmak için çalışma sonunda bir sentinin gönderildiği bir tüketici / üretici iş parçacığı zinciridir .

C ++ destekliyorsa, kodunuzun şöyle görünmesini istersiniz:

    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);
        }
    }

Döngü çıktıktan sonra gerçekleştiğinden, sonunda bildirinizi döngü başlangıcına koymanın daha mantıklı olduğunu düşünüyorum ... ama bu arzulu bir düşünce çünkü C ++ ile yapamayız. Sıra o Not downstreamEğer sentinel içinde koyamazsınız yüzden, başka bir iş parçacığı bağlı olduğu push(nullptr)yıkıcı downstreambu noktada yok edilemez çünkü ... diğer iplik alana kadar hayatta kalmak gerekirnullptr .

İşte aynı şeyi yapmak için lambda ile bir RAII sınıfının nasıl kullanılacağı aşağıda açıklanmıştır:

    class Finally
    {
    public:

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

ve işte nasıl kullanıyorsunuz:

    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);
        }
    }

Merhaba, Yukarıdaki cevabımın ( stackoverflow.com/a/38701485/566849 ) gereksinimlerinizi tamamen karşıladığına inanıyorum .
Fabio A.

1

Birçok kişinin belirttiği gibi, çözüm sonunda blokları önlemek için C ++ 11 özelliklerini kullanmaktır. Özelliklerden biri unique_ptr.

İşte Mephane'nin yanıtı RAII kalıpları kullanılarak yazılmıştır.

#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);
}

C ++ Standart Kitaplık kapsayıcıları ile unique_ptr kullanmaya bazı girişler burada


0

Bir alternatif sunmak istiyorum.

Sonunda bloğun her zaman çağrılmasını istiyorsanız, sadece son yakalama bloğundan sonra koyun (muhtemelen catch( ... )bilinen istisnaları yakalamak olmalıdır )

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(); 

Sonunda herhangi bir istisna atıldığında yapılacak son şey olarak engellemek istiyorsanız, boole yerel değişkenini kullanabilirsiniz - çalıştırmadan önce false değerine ayarlayın ve try bloğunun sonuna gerçek atama koyun, sonra değişken için catch blok kontrolünden sonra değeri:

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();
}

Bu işe yaramaz, çünkü nihayet bloğun tüm noktası , kodun kod bloğundan çıkmasına izin vermesi gerektiğinde bile temizleme yapmaktır . Şunu düşünün: `{muhtemelen" B "atma şeyler"} catch (A & a) {} nihayet {// eğer C ++ buna sahipse ... // "B" atıldığında bile olması gereken şeyler. } // "B" atılırsa yürütülmez. `IMHO, istisnaların noktası hata işleme kodunu azaltmaktır , bu nedenle atışların meydana gelebileceği her yerde blokları yakalamak verimsizdir. Bu nedenle RAII yardımcı olur: serbestçe uygulanırsa, en üst ve alt katmanlardaki istisnalar önemlidir.
burlyearly

1
@burlyearly senin fikrin kutsal olmasa da, ben anladım, ama C ++ böyle bir şey bu yüzden bu davranışı taklit eden bir üst katman olarak düşünmek zorunda.
jave.web

İNDİR = LÜTFEN YORUMUN :)
jave.web

0

Ben de RIIA istisna işleme ve nihayet sahip olmak için tamamen yararlı bir yedek olmadığını düşünüyorum. BTW, ayrıca RIIA'nın her yerde kötü bir isim olduğunu düşünüyorum. Bu tür sınıflara 'kapıcı' diyorum ve onları çok kullanıyorum. Kaynakların ne başlatıyor ne de elde edildikleri zamanın% 95'i, kapsam dahilinde bazı değişiklikler uyguluyor veya önceden kurulmuş bir şeyi alıp yok edildiğinden emin oluyorlar. Bu resmi kalıp adı takıntılı internet bile benim adımı daha iyi olabilir önermek için istismar.

Sadece geçici bir şeyler listesinin her karmaşık kurulumunun, birden çok şeyi yakalama ihtiyacı karşısında hepsini temizlerken komplikasyonları önlemek için içerdiği bir sınıfa sahip olması gerektiğini makul görmüyorum. süreçte bir şeyler ters giderse istisna türleri. Bu, başka türlü gerekli olmayacak çok sayıda özel sınıfa yol açacaktır.

Evet, belirli bir kaynağı yönetmek için tasarlanmış sınıflar veya bir dizi benzer kaynağı işlemek için tasarlanmış genel sınıflar için uygundur. Ancak, ilgili tüm şeylerin bu tür sargıları olsa bile, temizlemenin koordinasyonu sadece yıkıcıların tersine çağrılması için basit olmayabilir.

Son olarak C ++ için mükemmel bir mantıklı olduğunu düşünüyorum. Yani, jeez, son on yıllar boyunca o kadar çok bit ve bobin yapıştırıldı ki, garip insanlar aniden oldukça yararlı olabilecek ve muhtemelen diğer bazı şeyler kadar karmaşık bir şey üzerinde muhafazakâr görünecekler. (gerçi bu sadece benim açımdan bir tahmin.)


-2
try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}

35
Sevimli deyim, ama aynı değil. try bloğuna veya catch'e geri dönmek 'nihayet:' kodunuzdan geçmez.
Edward KMETT

10
Edward Kmett çok önemli bir ayrım getirdiği için bu yanlış cevabı (0 puanla) tutmaya değer.
Mark Lakata

12
Daha da büyük bir kusur (IMO): Bu kod, tüm istisnaları yiyor, bu finallyda yapmıyor.
Ben Voigt
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.