(h/t do czytelnika Koe za udostępnienie pierwszej wersji tej lekcji!)
Patrząc na typową deklarację funkcji, nie jest możliwe określenie, czy funkcja może zgłosić wyjątek, czy nie:
int doSomething(); // can this function throw an exception or not?Czy w powyższym przykładzie doSomething() można zgłosić wyjątek? To nie jest jasne. Ale odpowiedź jest ważna w niektórych kontekstach. W lekcji 27.8 — Niebezpieczeństwa i wady związane z wyjątkami opisaliśmy, jak wyjątek wyrzucony z destruktora podczas odwijania stosu powoduje zatrzymanie programu. Jeśli doSomething() może zgłosić wyjątek, wywołanie go z destruktora (lub innego miejsca, w którym zgłoszony wyjątek jest niepożądany) jest ryzykowne. Chociaż destruktor mógłby obsługiwać wyjątki przez doSomething() (aby wyjątki te nie rozprzestrzeniały się poza destruktor), musimy o tym pamiętać i upewnić się, że uwzględnimy wszystkie typy wyjątków, które mogą zostać zgłoszone.
Chociaż komentarze mogą pomóc w wyliczeniu, czy funkcja zgłasza wyjątki, czy nie (a jeśli tak, to jakiego rodzaju wyjątków), dokumentacja może się zestarzeć i nie ma wymuszania kompilatora dla komentarze.
Specyfikacje wyjątków to mechanizm językowy, który został pierwotnie zaprojektowany w celu dokumentowania rodzaju wyjątków, jakie funkcja może zgłosić w ramach specyfikacji funkcji. Chociaż większość specyfikacji wyjątków została już uznana za przestarzałą lub usunięta, w zamian dodano jedną przydatną specyfikację wyjątku, którą omówimy w tej lekcji.
Specyfikator noexcept
W C++ wszystkie funkcje są klasyfikowane jako nie rzucające lub potencjalnie rzucające. A nie rzucające funkcja obiecuje nie zgłaszać wyjątków widocznych dla osoby wywołującej. Funkcja potencjalnie rzucająca może generować wyjątki widoczne dla osoby wywołującej.
Aby zdefiniować funkcję jako nierzucającą, możemy użyć specyfikatora noexcept. Aby to zrobić, używamy słowa kluczowego noexcept w deklaracji funkcji, umieszczonego po prawej stronie listy parametrów funkcji:
void doSomething() noexcept; // this function is specified as non-throwingZauważ to noexcept w rzeczywistości nie zapobiega to rzucaniu przez funkcję wyjątków ani wywoływaniu innych funkcji, które mogą powodować zgłaszanie. Jest to dozwolone pod warunkiem, że funkcja noexcept przechwytuje i obsługuje te wyjątki wewnętrznie, a wyjątki te nie opuszczają funkcji noexcept.
Jeśli nieobsługiwany wyjątek spowodowałby wyjście z funkcji noexcept, std::terminate zostanie wywołany (nawet jeśli istnieje procedura obsługi wyjątku, która w przeciwnym razie obsłużyłaby taki wyjątek gdzieś na stosie). A jeśli std::terminate zostanie wywołane z wnętrza funkcji noexcept, odwijanie stosu może nastąpić lub nie (w zależności od implementacji i optymalizacji), co oznacza, że obiekty mogą zostać poprawnie zniszczone przed zakończeniem lub nie.
Kluczowa informacja
Obiecanie, że funkcja noexcept składa, że nie będzie zgłaszać wyjątków widocznych dla osoby wywołującej, jest obietnicą umowną, a nie obietnicą narzuconą przez kompilator. Tak więc, chociaż wywoływanie funkcji noexcept powinno być bezpieczne, wszelkie błędy związane z obsługą wyjątków w funkcji noexcept, które powodują zerwanie kontraktu, spowodują zakończenie programu! Nie powinno się to zdarzać, ale błędy też nie powinny.
Z tego powodu najlepiej jest, aby funkcje noexcept w ogóle nie mieszały się z wyjątkami lub nie wywoływały funkcji potencjalnie rzucających, które mogłyby zgłosić wyjątek. Funkcja noexcept nie może mieć błędu związanego z obsługą wyjątków, jeśli w ogóle nie można zgłosić żadnych wyjątków!
Podobnie jak funkcje, które różnią się tylko wartościami zwracanymi, nie mogą być przeciążane, tak funkcje różniące się jedynie specyfikacją wyjątków nie mogą być przeciążane.
Ilustrowanie zachowania funkcji noexcept i wyjątków
Poniższy program ilustruje zachowanie funkcji noexcept i wyjątków w różnych przypadkach:
// h/t to reader yellowEmu for the first draft of this program
#include <iostream>
class Doomed
{
public:
~Doomed()
{
std::cout << "Doomed destructed\n";
}
};
void thrower()
{
std::cout << "Throwing exception\n";
throw 1;
}
void pt()
{
std::cout << "pt (potentally throwing) called\n";
//This object will be destroyed during stack unwinding (if it occurs)
Doomed doomed{};
thrower();
std::cout << "This never prints\n";
}
void nt() noexcept
{
std::cout << "nt (noexcept) called\n";
//This object will be destroyed during stack unwinding (if it occurs)
Doomed doomed{};
thrower();
std::cout << "this never prints\n";
}
void tester(int c) noexcept
{
std::cout << "tester (noexcept) case " << c << " called\n";
try
{
(c == 1) ? pt() : nt();
}
catch (...)
{
std::cout << "tester caught exception\n";
}
}
int main()
{
std::cout << std::unitbuf; // flush buffer after each insertion
std::cout << std::boolalpha; // print boolean as true/false
tester(1);
std::cout << "Test successful\n\n";
tester(2);
std::cout << "Test successful\n";
return 0;
}Na maszynie autora program ten wydrukował:
tester (noexcept) case 1 called pt (potentially throwing) called Throwing exception Doomed destructed tester caught exception Test successful tester (noexcept) case 2 called nt (noexcept) called throwing exception terminate called after throwing an instance of 'int'
, a następnie program został przerwany.
Przyjrzyjmy się, co się tutaj dzieje bardziej szczegółowo. Zauważ, że tester jest funkcją noexcept, a zatem obiecuje nie ujawniać żadnego wyjątku wywołującemu (main).
Pierwszy przypadek ilustruje, że funkcje noexcept mogą wywoływać funkcje potencjalnie rzucające, a nawet obsługiwać wszelkie wyjątki zgłaszane przez te funkcje. Najpierw wywoływana jest funkcja tester(1) , która wywołuje funkcję potencjalnie rzucającą pt , która wywołuje thrower, która zgłasza wyjątek. Pierwsza procedura obsługi tego wyjątku znajduje się w tester, więc wyjątek rozwija stos (niszcząc zmienną lokalną doomed w procesie), a wyjątek jest przechwytywany i obsługiwany w tester. Ponieważ tester nie ujawnia tego wyjątku wywołującemu (main), nie ma tutaj naruszenia noexcept i sterowanie wraca do main.
Drugi przypadek ilustruje, co się dzieje, gdy funkcja noexcept próbuje przekazać wyjątek Najpierw wywoływany jest tester(2) , który wywołuje funkcję nie wywołującą nt, która wywołuje thrower, która zgłasza wyjątek. Pierwsza procedura obsługi tego wyjątku znajduje się w tester. Jednak nt is noexcept i aby dostać się do procedury obsługi w tester, wyjątek musiałby zostać rozpropagowany do osoby wywołującej nt. Jest to naruszenie noexcept nt, dlatego zostaje wywołane std::terminate , a nasz program zostaje natychmiast przerwany. Na maszynie autora stos nie został odwinięty (co ilustruje doomed niezniszczenie).
Specyfikator noexcept z parametrem Boolean
Klasa noexcept ma opcjonalny specyfikator Boolean parametr. noexcept(true) jest odpowiednikiem noexcept, co oznacza, że funkcja nie jest rzucana. noexcept(false) oznacza, że funkcja może rzucać. Parametry te są zazwyczaj używane tylko w funkcjach szablonowych, dzięki czemu funkcja szablonowa może być dynamicznie tworzona jako funkcja nierzucająca lub potencjalnie rzucająca w oparciu o pewną sparametryzowaną wartość.
Które funkcje nie rzucają, a potencjalnie rzucają
Funkcje, które są domyślnie bez rzucania:
- Destruktory
Funkcje, które domyślnie nie są zgłaszane dla funkcji zadeklarowanych domyślnie lub domyślnie:
- Konstruktory: default, copy, move
- Przypisania: kopiuj, przesuń
- Operatory porównania (od C++ 20)
Jeśli jednak którakolwiek z tych funkcji wywoła (jawnie lub pośrednio) inną funkcję, która potencjalnie rzuca, wówczas wymieniona funkcja również będzie traktowana jako potencjalnie rzucająca. Na przykład, jeśli klasa zawiera element danych z konstruktorem potencjalnie rzucającym, wówczas konstruktory klasy również będą traktowane jako potencjalnie rzucające. Inny przykład: jeśli operator przypisania kopiowania wywoła potencjalnie rzucający operator przypisania, wówczas przypisanie kopiujące również będzie potencjalnie rzucające.
Funkcje, które potencjalnie rzucają (jeśli nie są jawnie zadeklarowane lub domyślnie):
- Funkcje normalne
- Konstruktory zdefiniowane przez użytkownika
- Operatory zdefiniowane przez użytkownika
Operator noexcept
Operator noexcept może być również używany wewnątrz wyrażeń. Przyjmuje wyrażenie jako argument i zwraca true lub false jeśli kompilator uważa, że wyrzuci wyjątek, czy nie operator jest sprawdzany statycznie w czasie kompilacji i tak naprawdę nie ocenia wyrażenia wejściowego.
void foo() {throw -1;}
void boo() {};
void goo() noexcept {};
struct S{};
constexpr bool b1{ noexcept(5 + 3) }; // true; ints are non-throwing
constexpr bool b2{ noexcept(foo()) }; // false; foo() throws an exception
constexpr bool b3{ noexcept(boo()) }; // false; boo() is implicitly noexcept(false)
constexpr bool b4{ noexcept(goo()) }; // true; goo() is explicitly noexcept(true)
constexpr bool b5{ noexcept(S{}) }; // true; a struct's default constructor is noexcept by defaultOperatora noexcept można użyć do warunkowego wykonania kodu w zależności od tego, czy jest to potencjalnie rzucający kod, czy nie. Jest to wymagane w celu spełnienia pewnych gwarancji bezpieczeństwa związanych z wyjątkami, o których porozmawiamy w następnej sekcji.
Wyjątki gwarantują bezpieczeństwo
An wyjątek. gwarancja bezpieczeństwa to wytyczne umowne dotyczące zachowania funkcji lub klas w przypadku wystąpienia wyjątku. Istnieją cztery poziomy gwarancji bezpieczeństwa wyjątków:
- Brak gwarancji — nie ma gwarancji, co się stanie, jeśli zostanie zgłoszony wyjątek (np. klasa może pozostać w stanie bezużytecznym)
- Gwarancja podstawowa — jeśli zostanie zgłoszony wyjątek, nie nastąpi wyciek pamięci i obiekt będzie nadal użyteczny, ale program może pozostać w zmodyfikowanym stanie.
- Silna gwarancja — jeśli zostanie zgłoszony wyjątek, pamięć nie zostanie wyciekły i stan programu nie zostanie zmieniony. Oznacza to, że funkcja musi albo całkowicie się powieść, albo w przypadku niepowodzenia nie może mieć żadnych skutków ubocznych. Jest to łatwe, jeśli awaria nastąpi, zanim cokolwiek zostanie zmodyfikowane, ale można to również osiągnąć poprzez wycofanie wszelkich zmian, tak aby program powrócił do stanu sprzed awarii.
- Brak rzutu/gwarancja braku niepowodzenia — funkcja zawsze zakończy się sukcesem (no-fail) lub zakończy się niepowodzeniem bez zgłaszania wyjątku widocznego dla osoby wywołującej (no-throw). Wyjątki mogą być zgłaszane wewnętrznie, jeśli nie zostaną ujawnione. Specyfikator
noexceptodwzorowuje ten poziom gwarancji bezpieczeństwa wyjątku.
Przyjrzyjmy się bardziej szczegółowo gwarancjom braku błędów:
Gwarancja braku zgłoszeń: jeśli funkcja zawiedzie, nie zgłosi wyjątku. Zamiast tego zwróci kod błędu lub zignoruje problem. Gwarancje braku rzutów są wymagane podczas odwijania stosu, gdy wyjątek jest już obsługiwany; na przykład wszystkie destruktory powinny mieć gwarancję braku rzutów (podobnie jak wszelkie funkcje wywoływane przez te destruktory). Przykłady kodu, który powinien być niegenerujący:
- destruktory i funkcje zwalniania/czyszczenia pamięci
- funkcje, które muszą wywoływać funkcje no-throw wyższego poziomu
Gwarancja niezawodności: funkcja zawsze odniesie sukces w tym, co próbuje zrobić (a zatem nigdy nie ma potrzeby zgłaszania wyjątku, zatem no-fail jest nieco silniejszą formą no-throw). Przykłady kodu, który powinien działać niezawodnie:
- przenoszenie konstruktorów i przenoszenie przypisań (semantyka przenoszenia opisana w rozdziale 22)
- funkcjezamień
- wyczyść/usuń/zresetuj funkcje na kontenerach
- operacje na std::unique_ptr (również omówione w rozdziale 22)
- funkcje, które muszą wywoływać funkcje niezawodne wyższego poziomu
Kiedy używać noexcept
To, że Twój kod jawnie nie zgłasza żadnych wyjątków, nie oznacza, że powinieneś zacząć noexcept dosypywać kod wokół kodu funkcji, istnieje duża szansa, że wywoła funkcję, która potencjalnie rzuca, a zatem potencjalnie również rzuca.
Istnieje kilka dobrych powodów, aby oznaczyć funkcje jako nierzucające:
- Funkcje nie wywołujące można bezpiecznie wywoływać z funkcji, które nie są zabezpieczone przed wyjątkami, takich jak destruktory
- Funkcje, które nie są wyjątkiem, mogą umożliwić kompilatorowi wykonanie pewnych optymalizacji, które w innym przypadku nie byłyby dostępne, ponieważ a funkcja noexcept nie może zgłosić wyjątku poza funkcją, kompilator nie musi się martwić o utrzymanie stosu środowiska wykonawczego w stanie umożliwiającym odwijanie, co może pozwolić mu na szybsze generowanie kodu.
- Istnieją istotne przypadki, w których wiedza o funkcji noexcept pozwala nam tworzyć bardziej wydajne implementacje w naszym własnym kodzie: standardowe kontenery biblioteczne (takie jak
std::vector) są świadome noexcept i używają operatora noexcept do określenia, czy użyjmove semantics(szybciej) lubcopy semantics(wolniej) w niektórych miejscach. Semantykę ruchu omówimy w rozdziale 22, a tę optymalizację w lekcji 27.10 — std::move_if_noexcept.
Zasadą biblioteki standardowej jest używanie noexcept tylko w funkcjach, które muszą nie rzut lub porażka. Funkcje, które potencjalnie generują wyjątki, ale w rzeczywistości nie zgłaszają wyjątków (ze względu na implementację), zazwyczaj nie są oznaczone jako noexcept.
W przypadku własnego kodu zawsze oznaczaj następujące elementy jako noexcept:
- Przenieś konstruktory
- Przenieś operatory przypisania
- Zamień funkcje
W przypadku swojego kodu rozważ oznaczenie następujących funkcji jako noexcept:
- Funkcje, dla których chcesz wyrazić gwarancja braku rzutów lub braku błędów (np. w celu udokumentowania, że można je bezpiecznie wywołać z destruktorów lub innych funkcji noexcept)
- Kopiuj konstruktory i operatory przypisania kopiowania, które nie są wyrzucane (aby skorzystać z optymalizacji).
- Destruktory. Destruktory są domyślnie nieaktywne, chyba że wszyscy członkowie mają destruktory noexcept
Najlepsza praktyka
Zawsze twórz konstruktory przenoszenia, przypisania przenoszenia i funkcje zamiany noexcept.
Utwórz konstruktory kopiujące i operatory przypisania kopiowania noexcept kiedy możesz.
Użyj noexcept w innych funkcjach, aby wyrazić gwarancję braku błędów lub braku rzutów.
Najlepsza praktyka
Jeśli nie jesteś pewien, czy funkcja powinien mieć gwarancję, że nie ulegnie awarii/nie wyrzuci, zachowaj ostrożność i nie zaznaczaj tego symbolem noexcept. Cofnięcie decyzji o użyciu noexcept narusza zobowiązanie interfejsu wobec użytkownika dotyczące zachowania funkcji i może uszkodzić istniejący kod. Wzmacnianie gwarancji poprzez późniejsze dodanie noexcept do funkcji, która pierwotnie nie była noexcept, jest uważane za bezpieczne.
Specyfikacje wyjątków dynamicznych
Odczyt opcjonalny
Przed C++11 i do C++17, dynamicznych specyfikacji wyjątków były używane zamiast noexcept. Słowo kluczowe dynamicznych specyfikacji wyjątków zastosowań składni throw słowo kluczowe określające typy wyjątków, które funkcja może generować bezpośrednio lub pośrednio:
int doSomething() throw(); // does not throw exceptions
int doSomething() throw(std::out_of_range, int*); // may throw either std::out_of_range or a pointer to an integer
int doSomething() throw(...); // may throw anythingZe względu na takie czynniki, jak niekompletne implementacje kompilatora, pewna niezgodność z funkcjami szablonowymi, częste nieporozumienia dotyczące ich działania oraz fakt, że biblioteka standardowa w większości ich nie używała, specyfikacje wyjątków dynamicznych zostały uznane za przestarzałe w C++ 11 i usunięte z języków C++ 17 i C++ 20. Więcej kontekstu można znaleźć w tym dokumencie .

