Szukaj…
Łapanie wyjątków
Blok try/catch
służy do wychwytywania wyjątków. Kod w sekcji try
to kod, który może zgłosić wyjątek, a kod w klauzuli (klauzulach) catch
obsługuje wyjątek.
#include <iostream>
#include <string>
#include <stdexcept>
int main() {
std::string str("foo");
try {
str.at(10); // access element, may throw std::out_of_range
} catch (const std::out_of_range& e) {
// what() is inherited from std::exception and contains an explanatory message
std::cout << e.what();
}
}
Do obsługi wielu typów wyjątków można użyć wielu klauzul catch
. Jeśli występuje wiele klauzul catch
, mechanizm obsługi wyjątków próbuje dopasować je w kolejności ich pojawienia się w kodzie:
std::string str("foo");
try {
str.reserve(2); // reserve extra capacity, may throw std::length_error
str.at(10); // access element, may throw std::out_of_range
} catch (const std::length_error& e) {
std::cout << e.what();
} catch (const std::out_of_range& e) {
std::cout << e.what();
}
Klasy wyjątków wyprowadzone ze wspólnej klasy podstawowej można przechwycić za pomocą pojedynczej klauzuli catch
dla wspólnej klasy podstawowej. Powyższy przykład może zastąpić dwie klauzule catch
dla std::length_error
i std::out_of_range
pojedynczą klauzulą dla std:exception
:
std::string str("foo");
try {
str.reserve(2); // reserve extra capacity, may throw std::length_error
str.at(10); // access element, may throw std::out_of_range
} catch (const std::exception& e) {
std::cout << e.what();
}
Ponieważ klauzule catch
są wypróbowane w kolejności, należy najpierw napisać bardziej szczegółowe klauzule catch, w przeciwnym razie kod obsługi wyjątków może nigdy nie zostać wywołany:
try {
/* Code throwing exceptions omitted. */
} catch (const std::exception& e) {
/* Handle all exceptions of type std::exception. */
} catch (const std::runtime_error& e) {
/* This block of code will never execute, because std::runtime_error inherits
from std::exception, and all exceptions of type std::exception were already
caught by the previous catch clause. */
}
Inną możliwością jest moduł obsługi typu catch-all, który przechwyci każdy rzucony obiekt:
try {
throw 10;
} catch (...) {
std::cout << "caught an exception";
}
Wyjątek Rethrow (propaguj)
Czasami chcesz zrobić coś z wyjątkiem, który wychwytujesz (na przykład pisz, aby zalogować się lub wydrukować ostrzeżenie) i pozwól, aby bąbelkowała do górnego zakresu, który ma być obsługiwany. Aby to zrobić, możesz powtórzyć każdy wychwycony wyjątek:
try {
... // some code here
} catch (const SomeException& e) {
std::cout << "caught an exception";
throw;
}
Korzystanie z throw;
bez argumentów ponownie wyrzuci aktualnie złapany wyjątek.
Aby ponownie zarządzać std::exception_ptr
rethrow_exception
std::exception_ptr
rethrow_exception
biblioteka C ++ Standard ma funkcję rethrow_exception
której można użyć, włączając nagłówek <exception>
w swoim programie.
#include <iostream>
#include <string>
#include <exception>
#include <stdexcept>
void handle_eptr(std::exception_ptr eptr) // passing by value is ok
{
try {
if (eptr) {
std::rethrow_exception(eptr);
}
} catch(const std::exception& e) {
std::cout << "Caught exception \"" << e.what() << "\"\n";
}
}
int main()
{
std::exception_ptr eptr;
try {
std::string().at(1); // this generates an std::out_of_range
} catch(...) {
eptr = std::current_exception(); // capture
}
handle_eptr(eptr);
} // destructor for std::out_of_range called here, when the eptr is destructed
Funkcja Try Blocks W konstruktorze
Jedyny sposób na złapanie wyjątku na liście inicjalizatora:
struct A : public B
{
A() try : B(), foo(1), bar(2)
{
// constructor body
}
catch (...)
{
// exceptions from the initializer list and constructor are caught here
// if no exception is thrown here
// then the caught exception is re-thrown.
}
private:
Foo foo;
Bar bar;
};
Funkcja Try Block dla zwykłej funkcji
void function_with_try_block()
try
{
// try block body
}
catch (...)
{
// catch block body
}
Co jest równoważne z
void function_with_try_block()
{
try
{
// try block body
}
catch (...)
{
// catch block body
}
}
Zauważ, że w przypadku konstruktorów i destruktorów zachowanie jest inne, ponieważ blok przechwytywania i tak generuje wyjątek (złapany, jeśli nie ma innego rzutu w korpusie bloku przechwytywania).
Funkcja main
może mieć blok try funkcji jak każda inna funkcja, ale blok try funkcji main
nie przechwytuje wyjątków, które występują podczas budowy nielokalnej zmiennej statycznej lub zniszczenia dowolnej zmiennej statycznej. Zamiast tego wywoływane jest std::terminate
.
Funkcja Try Blocks In destructor
struct A
{
~A() noexcept(false) try
{
// destructor body
}
catch (...)
{
// exceptions of destructor body are caught here
// if no exception is thrown here
// then the caught exception is re-thrown.
}
};
Zauważ, że chociaż jest to możliwe, należy bardzo uważać na rzucanie z destruktora, tak jakby wywoływany przez destruktor podczas rozwijania stosu wyjątek był wywoływany, std::terminate
.
Najlepsza praktyka: rzut według wartości, złap według stałej odniesienia
Zasadniczo za dobrą praktykę uważa się rzucanie według wartości (zamiast wskaźnika), ale przechwytywanie według (stałej) referencji.
try {
// throw new std::runtime_error("Error!"); // Don't do this!
// This creates an exception object
// on the heap and would require you to catch the
// pointer and manage the memory yourself. This can
// cause memory leaks!
throw std::runtime_error("Error!");
} catch (const std::runtime_error& e) {
std::cout << e.what() << std::endl;
}
Jednym z powodów, dla których łapanie przez referencję jest dobrą praktyką jest to, że eliminuje potrzebę rekonstrukcji obiektu, gdy jest przekazywany do bloku catch (lub podczas propagacji do innych bloków catch). Łapanie przez odniesienie pozwala również na obsługę wyjątków polimorficznie i pozwala uniknąć krojenia obiektów. Jeśli jednak ponownie zgłaszasz wyjątek (jak throw e;
patrz przykład poniżej), nadal możesz uzyskać wycinanie obiektów, ponieważ throw e;
instrukcja tworzy kopię wyjątku, ponieważ niezależnie od deklarowanego typu:
#include <iostream>
struct BaseException {
virtual const char* what() const { return "BaseException"; }
};
struct DerivedException : BaseException {
// "virtual" keyword is optional here
virtual const char* what() const { return "DerivedException"; }
};
int main(int argc, char** argv) {
try {
try {
throw DerivedException();
} catch (const BaseException& e) {
std::cout << "First catch block: " << e.what() << std::endl;
// Output ==> First catch block: DerivedException
throw e; // This changes the exception to BaseException
// instead of the original DerivedException!
}
} catch (const BaseException& e) {
std::cout << "Second catch block: " << e.what() << std::endl;
// Output ==> Second catch block: BaseException
}
return 0;
}
Jeśli masz pewność, że nie zamierzasz nic robić, aby zmienić wyjątek (np. Dodać informacje lub zmodyfikować wiadomość), przechwytywanie według stałej odwołania pozwala kompilatorowi na optymalizacje i może poprawić wydajność. Ale nadal może to powodować splatanie obiektów (jak pokazano w powyższym przykładzie).
Ostrzeżenie: Wystrzegaj się zgłaszania niezamierzonych wyjątków w blokach catch
, szczególnie związanych z przydzielaniem dodatkowej pamięci lub zasobów. Na przykład konstruowanie logic_error
, runtime_error
lub ich podklas może spowodować wyrzucenie bad_alloc
powodu bad_alloc
pamięci podczas kopiowania ciągu wyjątku, strumienie we / wy mogą wyrzucać podczas rejestrowania z ustawionymi odpowiednimi maskami wyjątków itp.
Zagnieżdżony wyjątek
Podczas obsługi wyjątków często zdarza się, że wychwytujesz ogólny wyjątek z funkcji niskiego poziomu (taki jak błąd systemu plików lub błąd przesyłania danych) i rzucasz bardziej szczegółowy wyjątek wysokiego poziomu, który wskazuje, że niektóre operacje wysokiego poziomu mogą nie można wykonać (np. nie można opublikować zdjęcia w Internecie). Umożliwia to obsługę wyjątków w celu reagowania na określone problemy z operacjami wysokiego poziomu, a także pozwala programistowi, po błędzie tylko na komunikat, znaleźć miejsce w aplikacji, w którym wystąpił wyjątek. Minusem tego rozwiązania jest to, że blok wywołań wyjątków jest obcinany, a oryginalny wyjątek zostaje utracony. Zmusza to programistów do ręcznego dołączania tekstu oryginalnego wyjątku do nowo utworzonego.
Zagnieżdżone wyjątki mają na celu rozwiązanie problemu poprzez dołączenie wyjątku niskiego poziomu, który opisuje przyczynę, do wyjątku wysokiego poziomu, który opisuje, co to znaczy w tym konkretnym przypadku.
std::nested_exception
pozwala zagnieżdżać wyjątki dzięki std::throw_with_nested
:
#include <stdexcept>
#include <exception>
#include <string>
#include <fstream>
#include <iostream>
struct MyException
{
MyException(const std::string& message) : message(message) {}
std::string message;
};
void print_current_exception(int level)
{
try {
throw;
} catch (const std::exception& e) {
std::cerr << std::string(level, ' ') << "exception: " << e.what() << '\n';
} catch (const MyException& e) {
std::cerr << std::string(level, ' ') << "MyException: " << e.message << '\n';
} catch (...) {
std::cerr << "Unkown exception\n";
}
}
void print_current_exception_with_nested(int level = 0)
{
try {
throw;
} catch (...) {
print_current_exception(level);
}
try {
throw;
} catch (const std::nested_exception& nested) {
try {
nested.rethrow_nested();
} catch (...) {
print_current_exception_with_nested(level + 1); // recursion
}
} catch (...) {
//Empty // End recursion
}
}
// sample function that catches an exception and wraps it in a nested exception
void open_file(const std::string& s)
{
try {
std::ifstream file(s);
file.exceptions(std::ios_base::failbit);
} catch(...) {
std::throw_with_nested(MyException{"Couldn't open " + s});
}
}
// sample function that catches an exception and wraps it in a nested exception
void run()
{
try {
open_file("nonexistent.file");
} catch(...) {
std::throw_with_nested( std::runtime_error("run() failed") );
}
}
// runs the sample function above and prints the caught exception
int main()
{
try {
run();
} catch(...) {
print_current_exception_with_nested();
}
}
Możliwe wyjście:
exception: run() failed
MyException: Couldn't open nonexistent.file
exception: basic_ios::clear
Jeśli pracujesz tylko z wyjątkami odziedziczonymi po std::exception
, kod można nawet uprościć.
std :: uncaught_exceptions
C ++ 17 wprowadza int std::uncaught_exceptions()
(w celu zastąpienia ograniczonego bool std::uncaught_exception()
), aby dowiedzieć się, ile wyjątków jest obecnie niewychwyconych. Pozwala to klasie ustalić, czy zostanie zniszczona podczas rozwijania stosu, czy nie.
#include <exception>
#include <string>
#include <iostream>
// Apply change on destruction:
// Rollback in case of exception (failure)
// Else Commit (success)
class Transaction
{
public:
Transaction(const std::string& s) : message(s) {}
Transaction(const Transaction&) = delete;
Transaction& operator =(const Transaction&) = delete;
void Commit() { std::cout << message << ": Commit\n"; }
void RollBack() noexcept(true) { std::cout << message << ": Rollback\n"; }
// ...
~Transaction() {
if (uncaughtExceptionCount == std::uncaught_exceptions()) {
Commit(); // May throw.
} else { // current stack unwinding
RollBack();
}
}
private:
std::string message;
int uncaughtExceptionCount = std::uncaught_exceptions();
};
class Foo
{
public:
~Foo() {
try {
Transaction transaction("In ~Foo"); // Commit,
// even if there is an uncaught exception
//...
} catch (const std::exception& e) {
std::cerr << "exception/~Foo:" << e.what() << std::endl;
}
}
};
int main()
{
try {
Transaction transaction("In main"); // RollBack
Foo foo; // ~Foo commit its transaction.
//...
throw std::runtime_error("Error");
} catch (const std::exception& e) {
std::cerr << "exception/main:" << e.what() << std::endl;
}
}
Wynik:
In ~Foo: Commit
In main: Rollback
exception/main:Error
Niestandardowy wyjątek
Nie powinieneś rzucać surowych wartości jako wyjątków, zamiast tego użyj jednej ze standardowych klas wyjątków lub stwórz własną.
Posiadanie własnej klasy wyjątków odziedziczonej po std::exception
jest dobrym sposobem na obejście tego. Oto niestandardowa klasa wyjątków, która bezpośrednio dziedziczy po std::exception
:
#include <exception>
class Except: virtual public std::exception {
protected:
int error_number; ///< Error number
int error_offset; ///< Error offset
std::string error_message; ///< Error message
public:
/** Constructor (C++ STL string, int, int).
* @param msg The error message
* @param err_num Error number
* @param err_off Error offset
*/
explicit
Except(const std::string& msg, int err_num, int err_off):
error_number(err_num),
error_offset(err_off),
error_message(msg)
{}
/** Destructor.
* Virtual to allow for subclassing.
*/
virtual ~Except() throw () {}
/** Returns a pointer to the (constant) error description.
* @return A pointer to a const char*. The underlying memory
* is in possession of the Except object. Callers must
* not attempt to free the memory.
*/
virtual const char* what() const throw () {
return error_message.c_str();
}
/** Returns error number.
* @return #error_number
*/
virtual int getErrorNumber() const throw() {
return error_number;
}
/**Returns error offset.
* @return #error_offset
*/
virtual int getErrorOffset() const throw() {
return error_offset;
}
};
Przykładowy rzut rzucający:
try {
throw(Except("Couldn't do what you were expecting", -12, -34));
} catch (const Except& e) {
std::cout<<e.what()
<<"\nError number: "<<e.getErrorNumber()
<<"\nError offset: "<<e.getErrorOffset();
}
Ponieważ nie tylko rzucasz głupi komunikat o błędzie, ale także niektóre inne wartości reprezentujące dokładnie ten błąd, obsługa błędów staje się znacznie bardziej wydajna i znacząca.
Istnieje klasa wyjątków, która pozwala ładnie obsługiwać komunikaty o błędach: std::runtime_error
Możesz także dziedziczyć po tej klasie:
#include <stdexcept>
class Except: virtual public std::runtime_error {
protected:
int error_number; ///< Error number
int error_offset; ///< Error offset
public:
/** Constructor (C++ STL string, int, int).
* @param msg The error message
* @param err_num Error number
* @param err_off Error offset
*/
explicit
Except(const std::string& msg, int err_num, int err_off):
std::runtime_error(msg)
{
error_number = err_num;
error_offset = err_off;
}
/** Destructor.
* Virtual to allow for subclassing.
*/
virtual ~Except() throw () {}
/** Returns error number.
* @return #error_number
*/
virtual int getErrorNumber() const throw() {
return error_number;
}
/**Returns error offset.
* @return #error_offset
*/
virtual int getErrorOffset() const throw() {
return error_offset;
}
};
Zauważ, że nie przesłoniłem funkcji what()
z klasy podstawowej ( std::runtime_error
), tzn. std::runtime_error
wersji what()
klasy podstawowej. Możesz to zmienić, jeśli masz dalszy plan.