Istnieje kilka sposobów, ale najpierw musisz zrozumieć, dlaczego czyszczenie obiektów jest ważne, a zatem powód std::exitjest marginalizowany wśród programistów C ++.
RAII i rozwijanie stosu
C ++ korzysta z idiomu zwanego RAII , co w uproszczeniu oznacza, że obiekty powinny wykonywać inicjalizację w konstruktorze i czyszczenie w destruktorze. Na przykład std::ofstreamklasa [może] otworzyć plik podczas konstruktora, a następnie użytkownik wykonuje na nim operacje wyjściowe, a na końcu pod koniec swojego cyklu życia, zwykle określanego przez jego zakres, wywoływany jest destruktor, który zasadniczo zamyka plik i opróżnia dowolna zapisana zawartość na dysk.
Co się stanie, jeśli nie dojdziesz do destruktora, aby opróżnić i zamknąć plik? Kto wie! Ale możliwe, że nie zapisze wszystkich danych, które miał zapisać w pliku.
Na przykład rozważ ten kod
#include <fstream>
#include <exception>
#include <memory>
void inner_mad()
{
throw std::exception();
}
void mad()
{
auto ptr = std::make_unique<int>();
inner_mad();
}
int main()
{
std::ofstream os("file.txt");
os << "Content!!!";
int possibility = /* either 1, 2, 3 or 4 */;
if(possibility == 1)
return 0;
else if(possibility == 2)
throw std::exception();
else if(possibility == 3)
mad();
else if(possibility == 4)
exit(0);
}
W każdej możliwości dzieje się:
- Możliwość 1: Return zasadniczo opuszcza bieżący zakres funkcji, więc wie o zakończeniu cyklu życia, w
osten sposób wywołując swój destruktor i wykonując prawidłowe czyszczenie przez zamknięcie i opróżnienie pliku na dysk.
- Możliwość 2: Zgłoszenie wyjątku zajmuje się także cyklem życia obiektów w bieżącym zakresie, tym samym wykonując prawidłowe czyszczenie ...
- Możliwość 3: Tutaj rozwija się rozwijanie stosu! Nawet jeśli zostanie zgłoszony wyjątek
inner_mad, odwijak przejdzie przez stos madi mainaby wykonać prawidłowe czyszczenie, wszystkie obiekty zostaną zniszczone poprawnie, w tym ptri os.
- Możliwość 4: Cóż, tutaj?
exitjest funkcją C i nie jest świadomy ani zgodny z idiomami C ++. To nie wykonywać porządki na swoich obiektach, w tym osw tym samym zakresie. Dlatego Twój plik nie zostanie poprawnie zamknięty i z tego powodu treść może nigdy nie zostać w nim zapisana!
- Inne możliwości: Po prostu opuści główny zakres, wykonując domniemanie,
return 0a tym samym dając taki sam efekt jak możliwość 1, tj. Prawidłowe czyszczenie.
Ale nie bądźcie tak pewni tego, co wam powiedziałem (głównie możliwości 2 i 3); kontynuuj czytanie, a dowiemy się, jak przeprowadzić prawidłowe czyszczenie oparte na wyjątkach.
Możliwe sposoby zakończenia
Wróć z głównego!
Powinieneś to zrobić, gdy tylko jest to możliwe; zawsze wolisz wracać ze swojego programu, zwracając odpowiedni status wyjścia z głównego.
Program wywołujący Twój program i być może system operacyjny mogą chcieć wiedzieć, czy to, co program miał zrobić, zostało wykonane pomyślnie, czy nie. Z tego samego powodu powinieneś zwrócić zero lub EXIT_SUCCESSaby zasygnalizować, że program został pomyślnie zakończony i EXIT_FAILUREaby zasygnalizować, że program zakończył się niepowodzeniem, każda inna forma wartości zwracanej jest zdefiniowana w implementacji ( § 18.5 / 8 ).
Jednak możesz być bardzo głęboko w stosie wywołań, a zwracanie wszystkiego może być bolesne ...
[Nie] zgłaszaj wyjątku
Zgłoszenie wyjątku wykona prawidłowe czyszczenie obiektu za pomocą rozwijania stosu, wywołując destruktor każdego obiektu w dowolnym poprzednim zakresie.
Ale oto haczyk ! Jest zdefiniowane w implementacji, czy odwijanie stosu jest wykonywane, gdy zgłoszony wyjątek nie jest obsługiwany (przez klauzulę catch (...)), a nawet jeśli masz noexceptfunkcję w środku stosu wywołań. Jest to określone w §15.5.1 [z wyjątkiem terminów] :
W niektórych sytuacjach należy zrezygnować z obsługi wyjątków dla mniej subtelnych technik obsługi błędów. [Uwaga: Te sytuacje to:
[...]
- gdy mechanizm obsługi wyjątków nie może znaleźć procedury obsługi zgłoszonego wyjątku (15.3), lub gdy wyszukiwanie funkcji obsługi (15.3) napotka najbardziej zewnętrzny blok funkcji o parametrze noexcept-specyfikacja , która nie zezwala na wyjątek (15.4), lub [...]
[...]
W takich przypadkach wywoływana jest funkcja std :: terminate () (18.8.3). W sytuacji, gdy nie znaleziono pasującego modułu obsługi, jest zdefiniowane w implementacji, czy stos jest rozwijany przed wywołaniem std :: terminate () [...]
Więc musimy to złapać!
Rzuć wyjątek i złap go w pozycji głównej!
Ponieważ niewyłapane wyjątki mogą nie powodować rozwijania stosu (a tym samym nie będą wykonywać właściwego czyszczenia) , powinniśmy wychwycić wyjątek w main, a następnie zwrócić status wyjścia ( EXIT_SUCCESSlub EXIT_FAILURE).
Prawdopodobnie dobrym rozwiązaniem byłoby:
int main()
{
/* ... */
try
{
// Insert code that will return by throwing a exception.
}
catch(const std::exception&) // Consider using a custom exception type for intentional
{ // throws. A good idea might be a `return_exception`.
return EXIT_FAILURE;
}
/* ... */
}
[Nie] std :: exit
Nie powoduje to żadnego rozwijania stosu i żaden żywy obiekt na stosie nie wywoła odpowiedniego niszczyciela w celu przeprowadzenia czyszczenia.
Jest to egzekwowane w §3.6.1 / 4 [basic.start.init] :
Zakończenie programu bez opuszczania bieżącego bloku (np. Przez wywołanie funkcji std :: exit (int) (18.5)) nie niszczy żadnych obiektów z automatycznym czasem przechowywania (12.4) . Jeśli std :: exit zostanie wywołane w celu zakończenia programu podczas niszczenia obiektu o czasie przechowywania statycznym lub wątkowym, program ma niezdefiniowane zachowanie.
Pomyśl o tym teraz, dlaczego zrobiłbyś coś takiego? Ile przedmiotów boleśnie uszkodziłeś?
Inne [złe] alternatywy
Istnieją inne sposoby zakończenia programu (inne niż zawieszanie się) , ale nie są zalecane. Dla wyjaśnienia zostaną tutaj przedstawione. Zwróć uwagę, że normalne zakończenie programu nie oznacza rozwijania stosu, ale dobry stan systemu operacyjnego.
std::_Exit powoduje normalne zakończenie programu i to wszystko.
std::quick_exitpowoduje normalne zakończenie programu i wywołuje std::at_quick_exitprocedury obsługi, żadne inne czyszczenie nie jest wykonywane.
std::exitpowoduje normalne zakończenie programu, a następnie wywołuje programy std::atexitobsługi. Przeprowadzane są inne rodzaje porządków, takie jak wywoływanie destrukcyjnych obiektów statycznych.
std::abortpowoduje nieprawidłowe zakończenie programu, żadne czyszczenie nie jest wykonywane. Należy to wywołać, jeśli program zakończy się w naprawdę, naprawdę nieoczekiwany sposób. Nie zrobi nic poza zasygnalizowaniem systemu operacyjnego o nienormalnym zakończeniu. W tym przypadku niektóre systemy wykonują zrzut pamięci.
std::terminatewywołuje te, std::terminate_handlerktóre std::abortdomyślnie wywołują .
main()użyciu return, w funkcjach użyj poprawnej wartości zwracanej lub wygeneruj odpowiedni wyjątek. Nie używajexit()!