Czy C ++ obsługuje bloki „w końcu”? (A o czym jest ten „RAII”, o którym wciąż słyszę?)

Odpowiedzi:

273

Nie, C ++ nie obsługuje bloków „nareszcie”. Powodem jest to, że C ++ zamiast tego obsługuje RAII: „Resource Acquisition Is Initialization” - kiepska nazwa dla naprawdę przydatnej koncepcji.

Chodzi o to, że destruktor obiektu jest odpowiedzialny za uwolnienie zasobów. Gdy obiekt ma automatyczny czas przechowywania, niszczyciel obiektu zostanie wywołany, gdy blok, w którym został utworzony, zostanie zamknięty - nawet gdy ten blok zostanie opuszczony w obecności wyjątku. Oto wyjaśnienie tego tematu przez Bjarne Stroustrup .

Częstym zastosowaniem RAII jest blokowanie muteksu:

// 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 upraszcza także używanie obiektów jako członków innych klas. Gdy klasa będąca właścicielem zostanie zniszczona, zasób zarządzany przez klasę RAII zostaje zwolniony, ponieważ w wyniku tego wywoływany jest destruktor dla klasy zarządzanej przez RAII. Oznacza to, że gdy używasz RAII dla wszystkich członków w klasie zarządzającej zasobami, możesz uniknąć bardzo prostego, może nawet domyślnego, destruktora dla klasy właściciela, ponieważ nie musi ręcznie zarządzać czasem życia zasobów członkowskich . (Podziękowania dla Mike'a B za zwrócenie na to uwagi.)

Dla tych, którzy znają C # lub VB.NET, możesz rozpoznać, że RAII jest podobny do deterministycznego niszczenia platformy .NET za pomocą instrukcji IDisposable i „using” . Rzeczywiście obie metody są bardzo podobne. Główną różnicą jest to, że RAII deterministycznie uwolni dowolny rodzaj zasobu - w tym pamięć. Podczas implementacji IDisposable w .NET (nawet w języku .NET C ++ / CLI) zasoby zostaną deterministycznie zwolnione, z wyjątkiem pamięci. W .NET pamięć nie jest zwalniana deterministycznie; pamięć jest zwalniana tylko podczas cykli odśmiecania.

 

† Niektórzy ludzie uważają, że „Zniszczenie to rezygnacja z zasobów” jest dokładniejszą nazwą idiomu RAII.

Kevin
źródło
18
„Zniszczenie to rezygnacja z zasobów” - DIRR ... Nie, nie działa dla mnie. = P
Erik Forbes,
14
RAII utknęło - tak naprawdę nie można tego zmienić. Próbowanie tego byłoby niemądre. Trzeba jednak przyznać, że „Pozyskiwanie zasobów to inicjalizacja” to wciąż dość kiepska nazwa.
Kevin
162
SBRM == Zarządzanie ograniczonymi zasobami zakresu
Johannes Schaub - litb
10
Każdy, kto ma umiejętności konstruowania nie tylko ogólnie oprogramowania, nie mówiąc już o ulepszonych technikach, nie może dać godnej wymówki dla tak przerażającego akronimu.
Hardryv
54
Powoduje to, że utkniesz, gdy masz coś do wyczyszczenia, który nie pasuje do życia żadnego obiektu C ++. Wydaje mi się, że kończysz na Lifetime Equals C ++ Liftime Orse Inse Gets Brzydko (LECCLEOEIGU?).
Warren P
79

W C ++ ostatecznie NIE jest wymagane z powodu RAII.

RAII przenosi odpowiedzialność za bezpieczeństwo wyjątku od użytkownika obiektu na projektanta (i implementatora) obiektu. Twierdzę, że jest to właściwe miejsce, ponieważ wystarczy raz uzyskać poprawność bezpieczeństwa wyjątku tylko raz (w projekcie / realizacji). Używając wreszcie, musisz uzyskać poprawne bezpieczeństwo wyjątku za każdym razem, gdy używasz obiektu.

Również IMO kod wygląda ładniej (patrz poniżej).

Przykład:

Obiekt bazy danych. Aby upewnić się, że używane jest połączenie DB, należy je otworzyć i zamknąć. Za pomocą RAII można to zrobić w konstruktorze / destruktorze.

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

Zastosowanie RAII sprawia, że ​​prawidłowe użycie obiektu DB jest bardzo łatwe. Obiekt DB poprawnie zamknie się za pomocą destruktora, bez względu na to, jak spróbujemy go nadużyć.

Java jak w końcu

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

Przy ostatecznym użyciu poprawne użycie obiektu zostaje przekazane użytkownikowi obiektu. tzn. użytkownik obiektu jest odpowiedzialny za prawidłowe jawne zamknięcie połączenia DB. Teraz możesz argumentować, że można to zrobić w finalizatorze, ale zasoby mogą mieć ograniczoną dostępność lub inne ograniczenia, a zatem ogólnie chcesz kontrolować uwalnianie obiektu, a nie polegać na niedeterministycznym zachowaniu modułu wyrzucającego śmieci.

To także prosty przykład.
Jeśli masz wiele zasobów, które należy zwolnić, kod może się skomplikować.

Bardziej szczegółową analizę można znaleźć tutaj: http://accu.org/index.php/journals/236

Martin York
źródło
16
// Make sure not to throw exception if one is already propagating.Ważne jest, aby destruktory C ++ również nie zgłaszały wyjątków z tego właśnie powodu.
Cemafor
10
@Cemafor: Przyczyna, dla której C ++ nie wyrzuca wyjątków z destruktora, jest inna niż Java. W Javie będzie działać (po prostu utracisz oryginalny wyjątek). W C ++ jest naprawdę źle. Ale w C ++ chodzi o to, że musisz to zrobić tylko raz (przez projektanta klasy), kiedy pisze on destruktor. W Javie musisz to zrobić w momencie użycia. Tak więc obowiązkiem użytkownika klasy jest napisanie tej samej płyty kotła bardzo szybko.
Martin York
1
Jeśli chodzi o bycie „potrzebnym”, nie potrzebujesz również RAII. Pozbądźmy się tego! :-) Żarty na bok, RAII jest w porządku w wielu przypadkach. RAII sprawia, że ​​bardziej kłopotliwe są przypadki, gdy chcesz wykonać jakiś kod (niezwiązany z zasobami), nawet jeśli powyższy kod zwrócił wcześniej. W tym celu użyj gotos lub podziel go na dwie metody.
Trinidad
1
@Trinidad: Nie jest to takie proste, jak myślisz (ponieważ wszystkie twoje sugestie wydają się wybierać najgorsze możliwe opcje). Dlatego pytanie może być lepszym miejscem do zbadania tego niż komentarze.
Martin York
1
Krytykowanie „NIE jest wymagane z powodu RAII”: istnieje wiele przypadków, w których dodanie ad-hoc RAII byłoby zbyt dużym kodem, aby dodać, a próba byłaby po prostu bardzo odpowiednia.
ceztko
63

RAII jest zwykle lepsze, ale można łatwo się wreszcie semantykę w C ++. Za pomocą niewielkiej ilości kodu.

Poza tym podstawowe wytyczne C ++ dają wreszcie.

Oto link do implementacji GSL Microsoft i link do implementacji Martin Moene

Bjarne Stroustrup wiele razy powiedział, że wszystko, co jest w GSL, w końcu oznaczało pójście w standard. Powinien to być w końcu przyszłościowy sposób użycia .

Możesz jednak łatwo wdrożyć się, jeśli chcesz, kontynuuj czytanie.

W C ++ 11 RAII i lambdas pozwala w końcu na generała:

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

przykład zastosowania:

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

wyjście będzie:

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

Osobiście użyłem tego kilka razy, aby zapewnić zamknięcie deskryptora pliku POSIX w programie C ++.

Posiadanie prawdziwej klasy, która zarządza zasobami i dzięki temu unika wszelkiego rodzaju wycieków, jest zwykle lepsze, ale w końcu jest to przydatne w przypadkach, w których tworzenie klasy brzmi jak przesada.

Poza tym, jak to lepiej niż inne języki wreszcie dlatego, jeżeli są stosowane w sposób naturalny piszesz kod zamykający pobliżu kodu otwierającego (w moim przykładzie ten nowy i kasowania ) i następuje zniszczenie konstrukcji w celu LIFO, jak zwykle w C ++. Jedynym minusem jest to, że dostajesz zmienną automatyczną, której tak naprawdę nie używasz, a składnia lambda sprawia, że ​​jest trochę głośna (w moim przykładzie w czwartym wierszu znaczenie ma tylko słowo w końcu i blok {} po prawej stronie mają znaczenie reszta to zasadniczo hałas).

Inny przykład:

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

Element wyłączający jest przydatny, jeśli w końcu należy wywołać tylko w przypadku awarii. Na przykład, musisz skopiować obiekt do trzech różnych pojemników, możesz w końcu skonfigurować, aby cofać każdą kopię i wyłączać po pomyślnym wykonaniu wszystkich kopii. Robiąc to, jeśli zniszczenie nie będzie możliwe, zapewnisz silną gwarancję.

wyłącz przykład:

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

Jeśli nie możesz użyć C ++ 11, możesz w końcu go mieć , ale kod staje się nieco bardziej skomplikowany. Wystarczy zdefiniować strukturę za pomocą tylko konstruktora i destruktora, konstruktor odwołuje się do wszystkiego, co jest potrzebne, a destruktor wykonuje niezbędne czynności. Jest to w zasadzie to, co robi lambda, wykonywane ręcznie.

#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
źródło
Może występować możliwy problem: w funkcji „w końcu (F f)” zwraca obiekt FinalAction, więc dekoder może zostać wywołany przed powrotem funkcji. Może powinniśmy użyć std :: function zamiast szablonu F.
user1633272
Zauważ, że FinalActionjest zasadniczo taki sam jak popularny ScopeGuardidiom, tylko z inną nazwą.
anderas
1
Czy ta optymalizacja jest bezpieczna?
Nulano,
2
@ Paolo.Bolzoni Przepraszam, że nie odpowiedziałem wcześniej, nie otrzymałem powiadomienia o twoim komentarzu. Martwiłem się, że w końcu blok (w którym wywołuję funkcję DLL) zostanie wywołany przed końcem zakresu (ponieważ zmienna jest nieużywana), ale od tego czasu znalazłem pytanie na temat SO, które wyjaśniło moje obawy. Odsyłam do niego link, ale niestety nie mogę go już znaleźć.
Nulano
1
Funkcja disable () jest rodzajem brodawki w twoim skądinąd czystym projekcie. Jeśli chcesz, aby w końcu wywoływano tylko w przypadku niepowodzenia, dlaczego po prostu nie użyć instrukcji catch? Czy nie po to jest?
user2445507
32

Oprócz ułatwienia czyszczenia obiektów opartych na stosie, RAII jest również użyteczny, ponieważ to samo „automatyczne” czyszczenie występuje, gdy obiekt należy do innej klasy. Gdy klasa będąca właścicielem zostanie zniszczona, zasób zarządzany przez klasę RAII zostaje wyczyszczony, ponieważ w wyniku tego wywoływany jest dtor dla tej klasy.

Oznacza to, że po osiągnięciu nirwany RAII i wszyscy członkowie klasy używają RAII (jak inteligentne wskaźniki), możesz uzyskać bardzo prosty (może nawet domyślny) dtor dla klasy właściciela, ponieważ nie musi ręcznie zarządzać jej okresy istnienia zasobów członkowskich.

Michael Burr
źródło
To bardzo dobra uwaga. +1 dla ciebie. Jednak niewiele osób głosowało na ciebie. Mam nadzieję, że nie masz nic przeciwko temu, że zredagowałem mój post, aby zamieścić twoje komentarze. (Dałem ci oczywiście kredyt.) Dzięki! :)
Kevin
30

dlaczego nawet zarządzane języki zapewniają ostateczny blok, mimo że i tak zasoby są automatycznie zwalniane przez moduł wyrzucania elementów bezużytecznych?

Właściwie, języki oparte na Garbage collectorach potrzebują „wreszcie” więcej. Śmieciarka nie niszczy obiektów w odpowiednim czasie, więc nie można polegać na prawidłowym usuwaniu problemów niezwiązanych z pamięcią.

Jeśli chodzi o dane przydzielane dynamicznie, wielu twierdzi, że powinieneś używać inteligentnych wskaźników.

Jednak...

RAII przenosi odpowiedzialność za bezpieczeństwo wyjątku od użytkownika obiektu na projektanta

Niestety jest to jego własny upadek. Stare nawyki programowania C umierają ciężko. Gdy korzystasz z biblioteki napisanej w C lub bardzo w stylu C, RAII nie będzie używane. Bez przepisywania całego interfejsu API, właśnie z tym musisz pracować. Wtedy brak „w końcu” naprawdę gryzie.

Philip Couling
źródło
13
Dokładnie ... RAII wydaje się miły z idealnej perspektywy. Ale muszę cały czas pracować z konwencjonalnymi interfejsami API C (jak funkcje typu C w Win32 API ...). Często zdarza się, że zdobywasz zasób, który zwraca pewien rodzaj UCHWYTU, który następnie wymaga funkcji takiej jak CloseHandle (UCHWYT) do czyszczenia. Korzystanie z try ... wreszcie jest dobrym sposobem radzenia sobie z możliwymi wyjątkami. (Na szczęście wygląda na to, że shared_ptr z niestandardowymi usuwaczami i lambda C ++ 11 powinny zapewnić pewną ulgę opartą na RAII, która nie wymaga pisania całych klas w celu zawinięcia API, których używam tylko w jednym miejscu.)
James Johnston,
7
@JamesJohnston, bardzo łatwo jest napisać klasę opakowania, która zawiera dowolny uchwyt i zapewnia mechanikę RAII. ATL zapewnia na przykład kilka z nich. Wydaje się, że uważasz to za zbyt wielki problem, ale nie zgadzam się, są bardzo małe i łatwe do napisania.
Mark Ransom
5
Proste tak, małe nie. Rozmiar zależy od złożoności biblioteki, z którą pracujesz.
Philip Couling,
1
@MarkRansom: Czy istnieje jakiś mechanizm, za pomocą którego RAII może zrobić coś inteligentnego, jeśli wyjątek wystąpi podczas czyszczenia, podczas gdy inny wyjątek jest w toku? W systemach z try / wreszcie możliwe jest - choć niewygodne - takie ułożenie, aby oczekujący wyjątek i wyjątek, który wystąpił podczas czyszczenia, zostały zapisane w nowym CleanupFailedException. Czy istnieje jakiś możliwy sposób na osiągnięcie takiego wyniku przy użyciu RAII?
supercat
3
@couling: Istnieje wiele przypadków, w których program wywoła SomeObject.DoSomething()metodę i chce wiedzieć, czy (1) się udało, (2) nie powiodło się bez efektów ubocznych , (3) nie powiodło się ze skutkami ubocznymi, z którymi program wywołujący jest przygotowany lub (4) nie powiodło się z efektami ubocznymi, z którymi osoba dzwoniąca nie może sobie poradzić. Tylko dzwoniący będzie wiedział, z jakimi sytuacjami może sobie poradzić; to, czego potrzebuje dzwoniący, to sposób na poznanie sytuacji. Szkoda, że ​​nie ma standardowego mechanizmu dostarczania najważniejszych informacji o wyjątku.
supercat,
9

Kolejna emulacja bloku „w końcu” za pomocą funkcji lambda C ++ 11

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

Miejmy nadzieję, że kompilator zoptymalizuje powyższy kod.

Teraz możemy napisać taki kod:

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

Jeśli chcesz, możesz zawinąć ten idiom w makra „spróbuj - w końcu”:

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

Teraz blok „w końcu” jest dostępny w 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

Osobiście nie podoba mi się wersja „w końcu” „makro” i wolałbym używać czystej funkcji „with_finally”, nawet jeśli składnia jest w tym przypadku bardziej nieporęczna.

Możesz przetestować powyższy kod tutaj: http://coliru.stacked-crooked.com/a/1d88f64cb27b3813

PS

Jeśli potrzebujesz wreszcie zablokować w kodzie, a następnie scoped strażników lub ON_FINALLY / ON_EXCEPTION makra będzie prawdopodobnie lepiej dopasowane do Twoich potrzeb.

Oto krótki przykład użycia 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
źródło
1
Pierwszy to dla mnie najbardziej czytelna ze wszystkich opcji przedstawionych na tej stronie. +1
Nikos
7

Przepraszamy za wykopanie tak starego wątku, ale w następującym uzasadnieniu występuje poważny błąd:

RAII przenosi odpowiedzialność za bezpieczeństwo wyjątku od użytkownika obiektu na projektanta (i implementatora) obiektu. Twierdzę, że jest to właściwe miejsce, ponieważ wystarczy raz uzyskać poprawność bezpieczeństwa wyjątku tylko raz (w projekcie / realizacji). Używając wreszcie, musisz uzyskać poprawne bezpieczeństwo wyjątku za każdym razem, gdy używasz obiektu.

Najczęściej musisz radzić sobie z dynamicznie przydzielanymi obiektami, dynamicznymi liczbami obiektów itp. W ramach try-block, niektóre kody mogą tworzyć wiele obiektów (ile jest określanych w czasie wykonywania) i przechowywać do nich wskaźniki na liście. To nie jest egzotyczny scenariusz, ale bardzo powszechny. W takim przypadku chciałbyś napisać coś takiego

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

Oczywiście sama lista zostanie zniszczona, gdy wyjdzie poza zakres, ale to nie wyczyści tymczasowych obiektów, które utworzyłeś.

Zamiast tego musisz wybrać brzydką drogę:

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

Ponadto: dlaczego nawet zarządzane sieci zapewniają ostateczny blok, mimo że zasoby są i tak automatycznie zwalniane przez śmieciarza?

Wskazówka: „w końcu” możesz zrobić więcej niż tylko zwolnienie pamięci.

Mefan
źródło
17
Języki zarządzane potrzebują ostatecznie bloków właśnie dlatego, że automatycznie zarządzany jest tylko jeden rodzaj zasobów: pamięć. RAII oznacza, że ​​wszystkie zasoby mogą być obsługiwane w ten sam sposób, więc nie ma takiej potrzeby. Jeśli faktycznie użyłeś RAII w swoim przykładzie (używając inteligentnych wskaźników na liście zamiast nagich), kod byłby prostszy niż twój „w końcu” -przykład. A nawet prościej, jeśli nie sprawdzisz wartości zwracanej przez new - sprawdzanie jej jest praktycznie bezcelowe.
Myto
7
newnie zwraca NULL, zamiast tego zgłasza wyjątek
Hasturkun
5
Podnosisz ważne pytanie, ale ma ono 2 możliwe odpowiedzi. Jednym z nich jest ten, który podaje Myto - używaj inteligentnych wskaźników do wszystkich dynamicznych alokacji. Drugim jest użycie standardowych pojemników, które zawsze niszczą ich zawartość po zniszczeniu. Tak czy inaczej, każdy przydzielony obiekt jest ostatecznie własnością statycznie przydzielonego obiektu, który automatycznie uwalnia go po zniszczeniu. Szkoda, że ​​programiści nie są w stanie odkryć tych lepszych rozwiązań ze względu na wysoką widoczność prostych wskaźników i tablic.
j_random_hacker
4
C ++ 11 poprawia to i obejmuje std::shared_ptri std::unique_ptrbezpośrednio w stdlib.
u0b34a0f6ae,
16
Twój przykład wygląda tak okropnie nie dlatego, że RAII jest wadliwy, a raczej dlatego, że go nie wykorzystałeś. Surowe wskaźniki nie są RAII.
Ben Voigt
6

FWIW, Microsoft Visual C ++ obsługuje wreszcie próbę, i w przeszłości był stosowany w aplikacjach MFC jako metoda wychwytywania poważnych wyjątków, które w przeciwnym razie spowodowałyby awarię. Na przykład;

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

W przeszłości korzystałem z tego, aby robić kopie zapasowe otwartych plików przed wyjściem. Niektóre ustawienia debugowania JIT spowodują jednak uszkodzenie tego mechanizmu.

SmacL
źródło
4
pamiętaj, że tak naprawdę nie są to wyjątki w C ++, ale SEH. Możesz używać obu w kodzie MS C ++. SEH to program obsługi wyjątków systemu operacyjnego, który jest sposobem implementacji wyjątków VB, .NET.
gbjbaanb
i możesz użyć SetUnhandledExceptionHandler, aby utworzyć „globalną” nieprzechwyconą procedurę obsługi wyjątków - dla wyjątków SEH.
gbjbaanb
3
SEH jest okropny i zapobiega również wywoływaniu destruktorów C ++
paulm
6

Jak wskazano w innych odpowiedziach, C ++ może obsługiwać finallypodobną funkcjonalność. Wdrożenie tej funkcjonalności, która prawdopodobnie jest najbliższa byciu częścią standardowego języka, jest tym towarzyszącym Podstawowym Wytycznym C ++ , zestawowi najlepszych praktyk korzystania z C ++ edytowanym przez Bjarne Stoustrup i Herb Sutter. Realizacjafinally jest częścią Wytycznych Wsparcia Library (GSL). W Wytycznych zaleca się korzystanie z finallyinterfejsów w starym stylu, a także ma własną wytyczną zatytułowaną Użyj obiektu aktywności końcowej do wyrażenia czyszczenia, jeśli nie jest dostępny odpowiedni uchwyt zasobów .

C ++ nie tylko obsługuje C ++ finally, ale zaleca się używanie go w wielu typowych przypadkach.

Przykład użycia implementacji GSL wyglądałby następująco:

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

Implementacja i użycie GSL jest bardzo podobne do tego w odpowiedzi Paolo.Bolzoni . Jedną różnicą jest to, że obiekt utworzony przez gsl::finally()brak disable()połączenia. Jeśli potrzebujesz tej funkcji (powiedzmy, aby zwrócić zasób po jego zmontowaniu i nie wystąpią żadne wyjątki), możesz preferować implementację Paolo. W przeciwnym razie korzystanie z GSL jest tak zbliżone do korzystania ze standardowych funkcji, jak to tylko możliwe.

tobi_s
źródło
3

Nie bardzo, ale możesz je naśladować do pewnego stopnia, na przykład:

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;

Zauważ, że blok w końcu może sam zgłosić wyjątek, zanim oryginalny wyjątek zostanie ponownie zgłoszony, odrzucając w ten sposób oryginalny wyjątek. Jest to dokładnie to samo zachowanie, co w przypadku ostatecznego bloku Java. Nie można także używać returnwewnątrz bloków try & catch.

bcmpinc
źródło
3
Cieszę się, że wspomniałeś, że w końcu blok może rzucić; jest to rzecz, którą większość odpowiedzi „użyj RAII” wydaje się ignorować. Aby uniknąć konieczności dwukrotnego pisania bloku std::exception_ptr e; try { /*try block*/ } catch (...) { e = std::current_exception(); } /*finally block*/ if (e) std::rethrow_exception(e);
końcowego
1
To wszystko, co chciałem wiedzieć! Dlaczego żadna z pozostałych odpowiedzi nie wyjaśniła, że ​​haczyk (...) + pusty rzut; działa prawie jak w końcu blok? Czasami potrzebujesz tego.
VinGarcia,
Rozwiązanie, które podałem w mojej odpowiedzi ( stackoverflow.com/a/38701485/566849 ) powinno pozwolić na zgłaszanie wyjątków od wewnątrz finallybloku.
Fabio A.,
3

Wymyśliłem finallymakro, które może być używane prawie jak ¹ finallysłowo kluczowe w Javie; korzysta z std::exception_ptrfunkcji lambda i znajomych, a std::promisewięc wymaga C++11lub jest wyższy; korzysta również z rozszerzenia GCC wyrażenia instrukcji złożonej , które jest również obsługiwane przez clang.

OSTRZEŻENIE : wcześniejsza wersja tej odpowiedzi wykorzystywała inną implementację koncepcji z wieloma dodatkowymi ograniczeniami.

Najpierw zdefiniujmy klasę pomocnika.

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

Następnie jest rzeczywiste makro.

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

Można go użyć w następujący sposób:

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

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

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

    finally {
        clean_it_all_up();
    }    
}

Użycie std::promisesprawia, że ​​bardzo łatwo jest go wdrożyć, ale prawdopodobnie wprowadza także sporo niepotrzebnych kosztów ogólnych, których można by uniknąć poprzez ponowne wdrożenie tylko potrzebnych funkcjonalności std::promise.


¹ CAVEAT: jest kilka rzeczy, które nie działają tak jak wersja Java finally. Z czubka mojej głowy:

  1. to nie jest możliwe, aby zerwać z zewnętrznej pętli z breakoświadczeniem od obrębie tryi catch()„s bloków, ponieważ żyją w obrębie funkcji lambda;
  2. po: musi być przynajmniej jeden catch()blok try: jest to wymaganie C ++;
  3. jeśli funkcja ma wartość zwracaną inną niż void, ale nie ma powrotu w blokach tryi catch()'s, kompilacja zakończy się niepowodzeniem, ponieważ finallymakro rozwinie się do kodu, który będzie chciał zwrócić a void. Może to być, cóż, pustka wydana przez posiadanie pewnego finally_noreturnrodzaju makra.

Podsumowując, nie wiem, czy sam bym tego użył, ale fajnie się z tym bawiłem. :)

Fabio A.
źródło
Tak, to był tylko szybki hack, ale jeśli programista wie, co robią, może to być przydatne.
Fabio A.
@MarkLakata, zaktualizowałem post o lepszą implementację, która obsługuje zgłaszanie wyjątków i zwrotów.
Fabio A.,
Wygląda dobrze. Możesz pozbyć się Caveat 2, po prostu umieszczając niemożliwy catch(xxx) {}blok na początku finallymakra, gdzie xxx jest fałszywym typem tylko dla celów posiadania co najmniej jednego bloku catch.
Mark Lakata
@MarkLakata, też o tym pomyślałem, ale uniemożliwiłoby to użycie catch(...), prawda?
Fabio A.,
Nie wydaje mi się Wystarczy utworzyć nieznany typ xxxw prywatnej przestrzeni nazw, która nigdy nie będzie używana.
Mark Lakata,
2

Mam przypadek użycia, w którym moim zdaniem finally powinna być całkowicie akceptowalna część języka C ++ 11, ponieważ uważam, że łatwiej jest go czytać z punktu widzenia przepływu. Mój przypadek użycia to łańcuch wątków konsumenta / producenta, w którym nullptrna końcu przebiegu wysyłany jest wartownik, aby zamknąć wszystkie wątki.

Jeśli C ++ to obsługuje, chciałbyś, aby Twój kod wyglądał następująco:

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

Myślę, że jest to bardziej logiczne niż umieszczenie deklaracji końcowej na początku pętli, ponieważ ma ona miejsce po zakończeniu pętli ... ale jest to myślenie życzeniowe, ponieważ nie możemy tego zrobić w C ++. Zauważ, że kolejka downstreamjest połączona z innym wątkiem, więc nie możesz umieścić wartownika push(nullptr)w niszczycielu, downstreamponieważ w tym momencie nie można go zniszczyć ... musi pozostać przy życiu, dopóki drugi wątek nie otrzyma nullptr.

Oto, jak użyć klasy RAII z ​​lambda, aby zrobić to samo:

    class Finally
    {
    public:

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

a oto jak z niego korzystasz:

    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
źródło
Cześć, wierzę, że moja powyższa odpowiedź ( stackoverflow.com/a/38701485/566849 ) w pełni spełnia twoje wymagania.
Fabio A.
1

Jak wiele osób stwierdziło, rozwiązaniem jest użycie funkcji C ++ 11, aby w końcu uniknąć blokowania. Jedną z funkcji jest unique_ptr.

Oto odpowiedź Mephane napisana przy użyciu wzorów 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);
}

Więcej informacji na temat korzystania z Unique_ptr w kontenerach biblioteki standardowej C ++ znajduje się tutaj

Mark Lakata
źródło
0

Chciałbym przedstawić alternatywę.

Jeśli chcesz, aby blok zawsze był wywoływany zawsze, po prostu wstaw go po ostatnim bloku catch (prawdopodobnie powinien to być catch( ... )złapany nieznany wyjątek)

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

Jeśli chcesz, aby blok był ostatnią rzeczą, którą należy zrobić po zgłoszeniu wyjątku, możesz użyć zmiennej lokalnej typu boolean - przed uruchomieniem ustaw wartość false i ustaw prawdziwe przypisanie na samym końcu bloku try, a następnie po sprawdzeniu bloku catch dla zmiennej wartość:

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
źródło
To nie działa, ponieważ cały punkt w końcu bloku polega na przeprowadzeniu czyszczenia, nawet jeśli kod powinien pozwolić wyjątkowi na opuszczenie bloku kodu. Zastanów się: `spróbuj {// rzeczy, które mogą wyrzucić„ B ”} catch (A i a) {} wreszcie {// jeśli C ++ to miało ... // rzeczy, które muszą się zdarzyć, nawet jeśli„ B ”zostanie wyrzucone. } // nie zostanie wykonane, jeśli zostanie rzucone „B”. `IMHO, celem wyjątków jest ograniczenie kodu obsługi błędów, więc łapanie bloków, gdziekolwiek może wystąpić rzut, przynosi efekt przeciwny do zamierzonego. Właśnie dlatego RAII pomaga: jeśli jest stosowany swobodnie, wyjątki mają największe znaczenie na górnej i dolnej warstwie.
burlyearly
1
@burlyearly, chociaż twoja opinia nie jest święta, rozumiem, ale w C ++ nie ma czegoś takiego, więc musisz rozważyć to jako górną warstwę, która naśladuje to zachowanie.
jave.web
DOWNVOTE = PROSZĘ KOMENTARZ :)
jave.web
0

Myślę również, że RIIA nie jest w pełni użytecznym zamiennikiem obsługi wyjątków i posiadania w końcu. BTW, myślę też, że RIIA to zła nazwa dookoła. Te rodzaje zajęć nazywam „dozorcami” i używam ich dużo. W 95% przypadków nie inicjują ani nie zdobywają zasobów, wprowadzają pewne zmiany na podstawie zakresu lub biorą coś już skonfigurowanego i upewniają się, że jest zniszczone. Jest to oficjalny Internet z obsesją na punkcie wzorca, którego używam, nawet sugerując, że moje imię może być lepsze.

Po prostu nie sądzę, aby uzasadnione było wymaganie, aby każda skomplikowana konfiguracja jakiejś listy ad hoc rzeczy musiała mieć napisaną klasę, aby ją zawierała, aby uniknąć komplikacji podczas czyszczenia wszystkiego w obliczu konieczności złapania wielu typy wyjątków, jeśli coś pójdzie nie tak. Doprowadziłoby to do wielu zajęć ad hoc, które inaczej nie byłyby konieczne.

Tak, dobrze jest w przypadku klas zaprojektowanych do zarządzania konkretnym zasobem lub ogólnych, które są zaprojektowane do obsługi zestawu podobnych zasobów. Ale nawet jeśli wszystkie zaangażowane rzeczy mają takie opakowania, koordynacja czyszczenia może nie być prostym wywołaniem niszczycieli w odwrotnej kolejności.

Wydaje mi się, że C ++ ma w końcu sens. Mam na myśli, Jezu, w ciągu ostatnich dziesięcioleci przyklejono do niego tak wiele drobiazgów, że wydaje się, że dziwni ludzie nagle stali się konserwatystami w stosunku do czegoś w końcu, co może być całkiem przydatne i prawdopodobnie nic tak skomplikowanego jak niektóre inne rzeczy, które zostały dodano (choć to tylko zgadywanie).

Dean Roddey
źródło
-2
try
{
  ...
  goto finally;
}
catch(...)
{
  ...
  goto finally;
}
finally:
{
  ...
}
Nieobsługiwany wyjątek
źródło
35
Ładny idiom, ale nie do końca taki sam. powrót do bloku try lub catch nie przejdzie przez kod „nareszcie:”.
Edward KMETT,
10
Warto zachować tę błędną odpowiedź (z oceną 0), ponieważ Edward Kmett wprowadza bardzo ważne rozróżnienie.
Mark Lakata,
12
Jeszcze większa wada (IMO): Ten kod zjada wszystkie wyjątki, co finallynie ma miejsca.
Ben Voigt