Dlaczego w C ++ nie ma konstrukcji „nareszcie”?

57

Obsługa wyjątków w C ++ jest ograniczona do try / throw / catch. W przeciwieństwie do Object Pascal, Java, C # i Python, nawet w C ++ 11 finallykonstrukcja nie została zaimplementowana.

Widziałem okropnie dużo literatury C ++ omawiającej „bezpieczny kod wyjątku”. Lippman pisze, że bezpieczny kod wyjątku jest ważnym, ale zaawansowanym, trudnym tematem, wykraczającym poza zakres jego Primer - co wydaje się sugerować, że bezpieczny kod nie jest fundamentalny dla C ++. Herb Sutter poświęca temu tematowi 10 rozdziałów w swoim wyjątkowym C ++!

Wydaje mi się jednak, że wiele problemów napotkanych podczas próby napisania „kodu bezpiecznego wyjątku” można by całkiem dobrze rozwiązać, gdyby finallykonstrukcja została zaimplementowana, co pozwala programiście zapewnić, że nawet w przypadku wyjątku program będzie można przywrócić do bezpiecznego, stabilnego i szczelnego stanu, blisko punktu alokacji zasobów i potencjalnie problematycznego kodu. Jako bardzo doświadczony programista Delphi i C # używam try .. wreszcie dość obszernie blokuje mój kod, podobnie jak większość programistów w tych językach.

Biorąc pod uwagę wszystkie „dzwonki i gwizdki” zaimplementowane w C ++ 11, zdziwiłem się, widząc, że „w końcu” nadal tam nie było.

Dlaczego więc finallykonstrukcja nigdy nie została zaimplementowana w C ++? To naprawdę nie jest bardzo trudna ani zaawansowana koncepcja do zrozumienia i bardzo pomaga w pisaniu „wyjątkowego bezpiecznego kodu”.

Wektor
źródło
25
Dlaczego nie w końcu? Ponieważ uwalniasz rzeczy w destruktorze, który uruchamia się automatycznie, gdy obiekt (lub inteligentny wskaźnik) opuści zakres. Destruktory są lepsze niż w końcu {}, ponieważ oddziela przepływ pracy od logiki czyszczenia. Tak jak nie chciałbyś, aby wywołania free () zaśmiecały Twój przepływ pracy w języku odśmiecanym.
mike30
2
Zobacz także Czy twórcy oprogramowania Java świadomie porzucili RAII?
BlueRaja - Danny Pflughoeft
8
Zadanie pytania: „Dlaczego nie ma finallyC ++ i jakie techniki obsługi wyjątków są stosowane zamiast niego?” jest ważny i dotyczy tematu tej witryny. Myślę, że istniejące odpowiedzi obejmują to dobrze. Zmieniając to w dyskusję na temat: „Czy powody, dla których projektanci C ++ nie uwzględnili finallywartości, są warte?” i „Czy finallynależy dodać do C ++?” i prowadzenie dyskusji na temat komentarzy do pytania, a każda odpowiedź nie pasuje do modelu tej witryny pytań i odpowiedzi.
Josh Kelley,
2
Jeśli w końcu masz już problem z rozdzieleniem: główny blok kodu jest tutaj, a problem czyszczenia jest załatwiony tutaj.
Kaz
2
@Kaz. Różnica jest niejawna w porównaniu do jawnego czyszczenia. Destruktor zapewnia automatyczne czyszczenie podobne do tego, jak zwykły stary prymityw jest czyszczony, gdy wyskakuje ze stosu. Nie musisz wykonywać żadnych wyraźnych połączeń czyszczących i możesz skupić się na swojej podstawowej logice. Wyobraź sobie, jak zawiłe byłoby, gdybyś musiał wyczyścić prymitywy przydzielone do stosu w try / wreszcie. Domniemane sprzątanie jest lepsze. Porównanie składni klas z funkcjami anonimowymi nie jest istotne. Chociaż przekazanie funkcji pierwszej klasy do funkcji zwalniającej uchwyt może scentralizować ręczne czyszczenie.
mike30

Odpowiedzi:

57

Jako dodatkowy komentarz do odpowiedzi @ Nemanja (która, cytując Stroustrupa, naprawdę jest tak dobrą odpowiedzią, jak to tylko możliwe):

To naprawdę tylko kwestia zrozumienia filozofii i idiomów C ++. Weźmy przykład operacji, która otwiera połączenie z bazą danych trwałej klasy i musi upewnić się, że to połączenie zostanie zamknięte, jeśli zostanie zgłoszony wyjątek. Jest to kwestia bezpieczeństwa wyjątków i dotyczy dowolnego języka z wyjątkami (C ++, C #, Delphi ...).

W języku używającym try/ finallykod może wyglądać mniej więcej tak:

database.Open();
try {
    database.DoRiskyOperation();
} finally {
    database.Close();
}

Prosty i bezpośredni. Istnieje jednak kilka wad:

  • Jeśli język nie ma deterministycznych destruktorów, zawsze muszę napisać finallyblok, w przeciwnym razie wycieknie zasoby.
  • Jeśli DoRiskyOperationjest więcej niż jedno wywołanie metody - jeśli mam trochę przetwarzania do wykonania w trybloku - wtedy Closeoperacja może skończyć się całkiem daleko od Openoperacji. Nie mogę napisać mojego czyszczenia tuż obok mojego nabycia.
  • Jeśli mam kilka zasobów, które muszą zostać pozyskane, a następnie uwolnione w sposób wyjątkowo bezpieczny, mogę skończyć z kilkoma warstwami głębokimi try/ finallybloków.

Podejście C ++ wygląda następująco:

ScopedDatabaseConnection scoped_connection(database);
database.DoRiskyOperation();

To całkowicie rozwiązuje wszystkie wady tego finallypodejścia. Ma kilka wad, ale są one stosunkowo niewielkie:

  • Jest duża szansa, że ​​musisz sam napisać ScopedDatabaseConnectionlekcję. Jest to jednak bardzo prosta implementacja - tylko 4 lub 5 linii kodu.
  • Polega ona na utworzeniu dodatkowej zmiennej lokalnej - której najwyraźniej nie jesteś fanem, na podstawie twojego komentarza o „ciągłym tworzeniu i niszczeniu klas polegających na ich destrukterach w celu oczyszczenia twojego bałaganu jest bardzo słaba” - ale dobry kompilator zoptymalizuje poza wszelką dodatkową pracą związaną z dodatkową zmienną lokalną. Dobry projekt C ++ opiera się w dużej mierze na tego rodzaju optymalizacjach.

Osobiście, biorąc pod uwagę te zalety i wady, uważam RAII za technikę o wiele bardziej preferowaną finally. Twój przebieg może się różnić.

Wreszcie, ponieważ RAII jest tak dobrze ugruntowanym idiomem w C ++, i aby uwolnić programistów od ciężaru pisania licznych Scoped...klas, istnieją biblioteki takie jak ScopeGuard i Boost.ScopeExit, które ułatwiają tego rodzaju deterministyczne porządki.

Josh Kelley
źródło
8
C # ma usinginstrukcję, która automatycznie czyści każdy obiekt implementujący IDisposableinterfejs. Tak więc, chociaż można to zrobić źle, bardzo łatwo jest to naprawić.
Robert Harvey
18
Konieczność napisania całkowicie nowej klasy, która zajmie się odwracaniem tymczasowej zmiany stanu, za pomocą idiomu projektu, który jest implementowany przez kompilator z try/finallykonstrukcją, ponieważ kompilator nie ujawnia try/finallykonstrukcji, a jedynym sposobem na uzyskanie dostępu jest dostęp przez klasę idiom projektowy nie jest „zaletą”; jest to definicja inwersji abstrakcji.
Mason Wheeler,
15
@MasonWheeler - Umm, nie powiedziałem, że konieczność napisania nowej klasy jest zaletą. Powiedziałem, że to wada. W sumie wolę RAII niż konieczność używania finally. Jak powiedziałem, twój przebieg może się różnić.
Josh Kelley,
7
@JoshKelley: „Dobry projekt w C ++ w dużej mierze opiera się na tego rodzaju optymalizacjach”. Pisanie kawałków obcego kodu, a następnie poleganie na optymalizacji kompilatora to Good Design ?! IMO to przeciwieństwo dobrego projektu. Podstawą dobrego projektowania jest zwięzły, czytelny kod. Mniej do debugowania, mniej do utrzymywania itp. Itp. NIE powinieneś pisać kawałków kodu, a następnie polegać na kompilatorze, aby wszystko zniknęło - IMO nie ma żadnego sensu!
Wektor
14
@Mikey: Czyli powielanie kodu czyszczenia (lub fakt, że czyszczenie musi nastąpić) w całej bazie kodu jest „zwięzłe” i „łatwe do odczytania”? Dzięki RAII piszesz taki kod jeden raz i jest on automatycznie stosowany wszędzie.
Mankarse
55

Od Dlaczego C ++ nie zapewnia konstrukcji „nareszcie”? w Bjarne Stroustrup w C ++ Style i technika FAQ :

Ponieważ C ++ obsługuje alternatywę, która jest prawie zawsze lepsza: technika „pozyskiwania zasobów to inicjalizacja” (TC ++ PL3 sekcja 14.4). Podstawową ideą jest reprezentowanie zasobu przez obiekt lokalny, aby destruktor obiektu lokalnego zwolnił zasób. W ten sposób programista nie może zapomnieć o zwolnieniu zasobu.

Nemanja Trifunovic
źródło
5
Ale nie ma nic w tej technice, która jest specyficzna dla C ++, prawda? Możesz wykonywać RAII w dowolnym języku za pomocą obiektów, konstruktorów i destruktorów. To świetna technika, ale samo istnienie RAII nie oznacza, że finallykonstrukcja jest zawsze bezużyteczna na zawsze, pomimo tego , co mówi Strousup. Sam fakt, że pisanie „kodu bezpieczeństwa wyjątkowego” to wielka sprawa w C ++, jest tego dowodem. Do cholery, C # ma oba destruktory i finally, i oba się przyzwyczajają.
Tacroy
28
@Tacroy: C ++ jest jednym z niewielu języków głównego nurtu, który ma deterministyczne czynniki destrukcyjne. „Destruktory” w C # są bezużyteczne do tego celu i musisz ręcznie napisać „używając” bloków, aby mieć RAII.
Nemanja Trifunovic
15
@ Mikey, masz odpowiedź: „Dlaczego C ++ nie zapewnia konstrukcji„ nareszcie ”?” bezpośrednio od samego Stroustrupa. O co więcej można prosić? Który jest dlaczego.
5
@Mikey Jeśli martwisz się o swój kod zachowuje się dobrze, w szczególności zasobów nie przecieka, gdy wyjątki są wyrzucane na niego, to obawy o bezpieczeństwo wyjątków / próbuje napisać wyjątkiem bezpiecznego kodu. Po prostu tego nie nazywasz, a ponieważ dostępne są różne narzędzia, wdrażasz to inaczej. Ale dokładnie o tym mówią ludzie z C ++, kiedy omawiają bezpieczeństwo wyjątków.
19
@Kaz: Muszę tylko pamiętać, żeby raz wyczyścić w destruktorze, i odtąd po prostu używam obiektu. Muszę pamiętać o czyszczeniu w ostatnim bloku za każdym razem, gdy korzystam z operacji, która przydziela.
deworde
19

Powodem, dla którego C ++ nie ma, finallyjest to, że nie jest potrzebny w C ++. finallysłuży do wykonania kodu niezależnie od tego, czy wystąpił wyjątek, czy nie, który prawie zawsze jest rodzajem kodu czyszczącego. W C ++ ten kod czyszczenia powinien znajdować się w destruktorze odpowiedniej klasy, a destruktor będzie zawsze wywoływany, podobnie jak finallyblok. Idiom użycia destruktora do czyszczenia nazywa się RAII .

W społeczności C ++ może być więcej dyskusji na temat kodu „bezpiecznego dla wyjątków”, ale jest on prawie równie ważny w innych językach, które mają wyjątki. Cały sens kodu „wyjątkowego” polega na tym, aby pomyśleć o tym, w jakim stanie kod zostanie pozostawiony, jeśli wystąpi wyjątek w dowolnej z wywoływanych funkcji / metod.
W C ++ kod „wyjątkowy” jest nieco ważniejszy, ponieważ C ++ nie ma automatycznego czyszczenia pamięci, które zajmuje się obiektami, które zostały osierocone z powodu wyjątku.

Powód, dla którego wyjątek dotyczący bezpieczeństwa jest omawiany bardziej w społeczności C ++, prawdopodobnie wynika również z faktu, że w C ++ musisz być bardziej świadomy tego, co może pójść nie tak, ponieważ w języku jest mniej domyślnych sieci bezpieczeństwa.

Bart van Ingen Schenau
źródło
2
Uwaga: Proszę nie twierdzić, że C ++ ma deterministyczne czynniki destrukcyjne. Obiekt Pascal / Delphi również ma deterministyczne destrukatory, ale obsługuje również „wreszcie”, z bardzo dobrych powodów, które wyjaśniłem w moich pierwszych komentarzach poniżej.
Wektor
13
@Mikey: Biorąc pod uwagę, że nigdy nie było propozycji dodania finallydo standardu C ++, myślę, że można bezpiecznie stwierdzić, że społeczność C ++ nie uważa the absence of finallyproblemu. W większości języków finallybrakuje konsekwentnego deterministycznego zniszczenia, jakie ma C ++. Widzę, że Delphi ma ich obu, ale nie znam wystarczająco historii, by wiedzieć, która była pierwsza.
Bart van Ingen Schenau
3
Dephi nie obsługuje obiektów opartych na stosie - tylko sterty i odwołania do obiektów na stosie. Dlatego też „w końcu” jest konieczne, aby w razie potrzeby jawnie wywołać destruktory itp.
Wektor
2
W C ++ jest mnóstwo cruftów, które prawdopodobnie nie są potrzebne, więc nie może to być właściwa odpowiedź.
Kaz
15
Przez ponad dwie dekady używałem tego języka i pracowałem z innymi osobami, które go używały, nigdy nie spotkałem działającego programisty C ++, który powiedziałby „Naprawdę chciałbym, żeby język miał finally”. Nigdy nie przypominam sobie żadnego zadania, które byłoby łatwiejsze, gdybym miał do niego dostęp.
Gort the Robot
12

Inni omawiali RAII jako rozwiązanie. To idealnie dobre rozwiązanie. Ale to tak naprawdę nie dotyczy tego , dlaczego nie dodali finallyrównież, ponieważ jest to bardzo pożądana rzecz. Odpowiedź na to pytanie jest bardziej fundamentalna przy projektowaniu i rozwoju C ++: zaangażowani w projektowanie C ++ zdecydowanie sprzeciwiali się wprowadzeniu funkcji projektowych, które można osiągnąć za pomocą innych funkcji bez większego zamieszania, a zwłaszcza tam, gdzie wymaga to wprowadzenia nowych słów kluczowych, które mogą spowodować niezgodność starszego kodu. Ponieważ RAII stanowi wysoce funkcjonalną alternatywę, finallya finallymimo to możesz samodzielnie tworzyć własne w C ++ 11, nie było potrzeby.

Wszystko, co musisz zrobić, to stworzyć klasę, Finallyktóra wywołuje funkcję przekazaną do konstruktora w destruktorze. Następnie możesz to zrobić:

try
{
    Finally atEnd([&] () { database.close(); });

    database.doRisky();
}

Jednak większość rodzimych programistów C ++ będzie preferować czyste obiekty RAII.

Jack Aidley
źródło
3
Brakuje przechwytywania odniesienia w lambda. Powinno być Finally atEnd([&] () { database.close(); });też, wyobrażam sobie, że lepiej jest: { Finally atEnd(...); try {...} catch(e) {...} }(Podniosłem finalizator z try-block, aby
wykonał
2

Możesz użyć wzorca „pułapki” - nawet jeśli nie chcesz używać bloku try / catch.

Umieść prosty obiekt w wymaganym zakresie. W destruktorze tego obiektu umieść swoją logikę „ostateczną”. Niezależnie od tego, kiedy stos zostanie rozwinięty, zostanie wywołany destruktor obiektu, a otrzymasz cukierki.

Arie R.
źródło
1
To nie odpowiada na pytanie, a jedynie dowodzi, że w końcu nie jest to wcale taki zły pomysł ...
Vector
2

Cóż, możesz coś w stylu roll-your-own finally, używając Lambdas, który uzyskałby następującą kompilację (używając przykładu bez RAII, oczywiście, nie najładniejszego kodu):

{
    FILE *file = fopen("test","w");

    finally close_the_file([&]{
        cout << "We're closing the file in a pseudo-finally clause." << endl;
        fclose(file);
    });
}

Zobacz ten artykuł .

einpoklum - przywróć Monikę
źródło
-2

Nie jestem pewien, czy zgadzam się z twierdzeniami, że RAII jest nadzbiorem finally. Pięta achillesowa RAII jest prosta: wyjątki. RAII jest implementowane za pomocą destruktorów i w C ++ zawsze jest wyrzucanie z destruktora. Oznacza to, że nie możesz używać RAII, gdy potrzebujesz do wyczyszczenia kodu czyszczenia. finallyZ drugiej strony, gdyby zostały wdrożone, nie ma powodu, aby sądzić, że rzucanie z finallybloku byłoby niezgodne z prawem .

Rozważ taką ścieżkę kodu:

void foo() {
    try {
        ... stuff ...
        complex_cleanup();
    } catch (A& a) {
        handle_a(a);
        complex_cleanup();
        throw;
    } catch (B& b) {
        handle_b(b);
        complex_cleanup();
        throw;
    } catch (...) {
        handle_generic();
        complex_cleanup();
        throw;
    }
}

Gdybyśmy mieli finally, moglibyśmy napisać:

void foo() {
    try {
        ... stuff ...
    } catch (A& a) {
        handle_a(a);
        throw;
    } catch (B& b) {
        handle_b(b);
        throw;
    } catch (...) {
        handle_generic();
        throw;
    } finally {
        complex_cleanup();
    }
}

Ale nie mogę znaleźć równoważnego zachowania przy użyciu RAII.

Jeśli ktoś wie, jak to zrobić w C ++, jestem bardzo zainteresowany odpowiedzią. Byłbym nawet zadowolony z czegoś, na czym polegał, na przykład wymuszając, że wszystkie wyjątki odziedziczone po jednej klasie z pewnymi specjalnymi możliwościami lub czymś takim.

Szalony naukowiec
źródło
1
W drugim przykładzie, jeśli complex_cleanupmożesz rzucić, możesz mieć przypadek, w którym dwa nieprzechwycone wyjątki są w locie, podobnie jak w przypadku RAII / destruktorów, a C ++ nie pozwala na to. Jeśli chcesz, aby oryginalny wyjątek był widoczny, complex_cleanuppowinien zapobiegać wszelkim wyjątkom, tak jak w przypadku RAII / destruktorów. Jeśli chcesz zobaczyć complex_cleanupwyjątek, myślę, że możesz użyć zagnieżdżonych bloków try / catch - chociaż jest to styczna i trudna do dopasowania w komentarzu, więc warto osobne pytanie.
Josh Kelley,
Chcę użyć RAII, aby uzyskać identyczne zachowanie jak w pierwszym przykładzie, bezpieczniej. Rzut w domniemanym finallybloku działałby wyraźnie tak samo, jak rzut w catchbloku WRT podczas lotu - nie sprawdza std::terminate. Pytanie brzmi „dlaczego nie finallyw C ++?” a wszystkie odpowiedzi mówią „nie potrzebujesz tego… RAII FTW!” Chodzi mi o to, że tak, RAII jest w porządku w prostych przypadkach, takich jak zarządzanie pamięcią, ale dopóki problem wyjątków nie zostanie rozwiązany, wymaga zbyt wiele przemyślenia / kosztów ogólnych / obaw / przeprojektowania, aby być rozwiązaniem ogólnego zastosowania.
MadScientist
3
Rozumiem twój punkt widzenia - istnieją pewne uzasadnione problemy z niszczycielami, które mogą rzucać - ale są one rzadkie. Mówienie, że wyjątki RAII + zawierają nierozwiązane problemy lub że RAII nie jest rozwiązaniem ogólnego zastosowania, po prostu nie pasuje do doświadczenia większości programistów C ++.
Josh Kelley
1
Jeśli czujesz potrzebę podniesienia wyjątków w destruktorach, robisz coś złego - prawdopodobnie używasz wskaźników w innych miejscach, gdy nie są one konieczne.
Wektor
1
To jest zbyt zaangażowane, by móc komentować. Zadaj pytanie na ten temat: Jak poradziłbyś sobie z tym scenariuszem w C ++ przy użyciu modelu RAII ... Wygląda na to, że nie działa ... Ponownie powinieneś skierować swoje komentarze : wpisz @ i nazwę członka, o którym mówisz na początku komentarza. Gdy komentarze dotyczą Twojego postu, otrzymasz powiadomienie o wszystkim, ale inni nie, chyba że skierujesz do nich komentarz.
Wektor