C++
Uitzonderingen
Zoeken…
Uitzonderingen vangen
Een try/catch
blok wordt gebruikt om uitzonderingen te vangen. De code in de try
sectie is de code die een uitzondering kan veroorzaken en de code in de catch
clausule (s) behandelt de uitzondering.
#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();
}
}
Meerdere catch
clausules kan worden gebruikt om meerdere soorten uitzondering te behandelen. Als er meerdere catch
clausules aanwezig zijn, de exception handling mechanisme probeert ze te passen in volgorde van hun verschijning in de code:
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();
}
Exceptionklassen die zijn afgeleid van een gemeenschappelijke basis klasse kan worden gevangen met een catch
clausule voor de gemeenschappelijke basisklasse. Het bovenstaande voorbeeld kunnen de twee vervangen catch
clausules voor std::length_error
en std::out_of_range
met een enkele clausule voor 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();
}
Omdat de catch
clausules worden berecht in orde is, moet u meer specifieke vangst clausules eerste schrijven, anders wordt uw exception handling code misschien nooit gecalled:
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. */
}
Een andere mogelijkheid is de catch-all handler, die elk gegooid object vangt:
try {
throw 10;
} catch (...) {
std::cout << "caught an exception";
}
Rethrow (propagate) uitzondering
Soms wilt u iets doen met de uitzondering die u opvangt (zoals schrijven om in het logboek te loggen of een waarschuwing af te drukken) en het laten opbellen naar het bovenste bereik dat moet worden afgehandeld. Om dit te doen, kunt u elke uitzondering die u vangt opnieuw bekijken:
try {
... // some code here
} catch (const SomeException& e) {
std::cout << "caught an exception";
throw;
}
Gebruik throw;
zonder argumenten zal de momenteel gevangen uitzondering opnieuw gooien.
Om een beheerde std::exception_ptr
te werpen, heeft de C ++ Standard Library de functie rethrow_exception
die kan worden gebruikt door de kop <exception>
in uw programma op te nemen.
#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
Functie Probeer blokken in constructor
De enige manier om uitzonderingen in de initialisatielijst te krijgen:
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;
};
Functie Probeer Blokkeren voor normale functie
void function_with_try_block()
try
{
// try block body
}
catch (...)
{
// catch block body
}
Dat is gelijk aan
void function_with_try_block()
{
try
{
// try block body
}
catch (...)
{
// catch block body
}
}
Merk op dat voor constructors en destructors het gedrag anders is omdat het vangblok toch een uitzondering opnieuw gooit (de gevangen als er geen andere worp in het lichaam van het vangblok is).
De functie main
mag een functie try-blok hebben zoals elke andere functie, maar main
functie try-blok van main
zal geen uitzonderingen opvangen die optreden tijdens de constructie van een niet-lokale statische variabele of de vernietiging van een statische variabele. In plaats daarvan wordt std::terminate
genoemd.
Functie Probeer blokken 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.
}
};
Merk op dat, hoewel dit mogelijk is, men heel voorzichtig moet zijn met het gooien van de destructor, alsof een destructor die tijdens het afwikkelen van de stapel wordt opgeroepen een uitzondering werpt, std::terminate
wordt aangeroepen.
Best practice: gooien op waarde, vangen op const referentie
Over het algemeen wordt het als een goede praktijk beschouwd om op waarde te gooien (in plaats van op aanwijzer), maar te vangen op basis van (const) referentie.
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;
}
Een reden waarom vangen door verwijzing een goede gewoonte is, is dat het de noodzaak elimineert om het object te reconstrueren wanneer het wordt doorgegeven aan het vangblok (of wanneer het doorgroeit naar andere vangblokken). Catching by reference maakt het ook mogelijk om de uitzonderingen polymorf te verwerken en vermijdt het snijden van objecten. Als u echter een uitzondering opnieuw aan het uitwerken bent (zoals throw e;
zie voorbeeld hieronder), kunt u nog steeds het object in plakken snijden omdat de throw e;
statement maakt een kopie van de uitzondering, ongeacht welk type wordt verklaard:
#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;
}
Als u zeker weet dat u niets gaat doen om de uitzondering te wijzigen (zoals informatie toevoegen of het bericht wijzigen), stelt het compileren door const referentie de compiler in staat om optimalisaties te maken en de prestaties te verbeteren. Maar dit kan nog steeds leiden tot het splitsen van objecten (zoals te zien in het bovenstaande voorbeeld).
Waarschuwing: pas op voor het gooien van onbedoelde uitzonderingen in catch
, vooral met betrekking tot het toewijzen van extra geheugen of bronnen. Bijvoorbeeld, het construeren van logic_error
, runtime_error
of hun subklassen kan bad_alloc
veroorzaken vanwege bad_alloc
geheugen bij het kopiëren van de uitzonderingsreeks, I / O-stromen kunnen tijdens het loggen worden gegooid met respectieve uitzonderingsmaskers ingesteld, enz.
Geneste uitzondering
Tijdens de afhandeling van uitzonderingen is er een veelvoorkomend geval wanneer u een generieke uitzondering van een functie op laag niveau (zoals een bestandssysteemfout of een fout bij gegevensoverdracht) ontdekt en een meer specifieke uitzondering op hoog niveau genereert die aangeeft dat een bewerking op hoog niveau kan niet worden uitgevoerd (zoals het niet kunnen publiceren van een foto op internet). Hierdoor kan de afhandeling van uitzonderingen reageren op specifieke problemen met bewerkingen op hoog niveau en kan de programmeur ook, met alleen een foutmelding, een plaats in de toepassing vinden waar een uitzondering is opgetreden. Nadeel van deze oplossing is dat callstack voor uitzonderingen is afgekapt en de oorspronkelijke uitzondering verloren is gegaan. Dit dwingt ontwikkelaars om de tekst van de oorspronkelijke uitzondering handmatig in een nieuw gemaakte tekst op te nemen.
Geneste uitzonderingen zijn bedoeld om het probleem op te lossen door een uitzondering op laag niveau, die de oorzaak beschrijft, toe te voegen aan een uitzondering op hoog niveau, die beschrijft wat het in dit specifieke geval betekent.
std::nested_exception
maakt het mogelijk om uitzonderingen te nestelen dankzij 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();
}
}
Mogelijke output:
exception: run() failed
MyException: Couldn't open nonexistent.file
exception: basic_ios::clear
Als u alleen werkt met uitzonderingen overgenomen van std::exception
, kan code zelfs worden vereenvoudigd.
std :: uncaught_exceptions
C ++ 17 introduceert int std::uncaught_exceptions()
(ter vervanging van de beperkte bool std::uncaught_exception()
) om te weten hoeveel uitzonderingen er momenteel niet worden opgevangen. Hiermee kan een klasse bepalen of deze tijdens een stapelafwikkeling wordt vernietigd of niet.
#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;
}
}
Output:
In ~Foo: Commit
In main: Rollback
exception/main:Error
Aangepaste uitzondering
U moet ruwe waarden niet als uitzonderingen gebruiken, maar in plaats daarvan een van de standaard uitzonderingsklassen gebruiken of uw eigen waarden maken.
Het hebben van uw eigen uitzonderingsklasse geërfd van std::exception
is een goede manier om dit aan te pakken. Hier is een aangepaste uitzonderingsklasse die rechtstreeks overneemt van 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;
}
};
Een voorbeeld werpvang:
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();
}
Omdat u niet alleen een domme foutmelding geeft, maar ook enkele andere waarden die aangeven wat de fout precies was, wordt uw foutafhandeling veel efficiënter en zinvoller.
Er is een uitzonderingsklasse waarmee je foutmeldingen netjes kunt afhandelen: std::runtime_error
Je kunt ook erven van deze klasse:
#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;
}
};
Merk op dat ik de functie what()
van de basisklasse ( std::runtime_error
) niet heb overschreven, dwz dat we de basisklasse-versie van what()
. Je kunt het overschrijven als je nog een agenda hebt.