Die Bibliothek Boost.Exception stellt den Ausnahmetyp boost::exception
zur Verfügung, mit dem es möglich ist, Daten einer Ausnahme hinzuzufügen, nachdem diese geworfen wurde. Die Klasse ist in der Headerdatei boost/exception/exception.hpp
definiert. Da Boost.Exception Klassen und Funktionen über mehrere Headerdateien verteilt, wird im Folgenden auf boost/exception/all.hpp
zugegriffen, ohne dass Headerdateien einzeln eingebunden werden müssen.
Boost.Exception unterstützt den seit C++11 im Standard verankerten Mechanismus, mit dem Ausnahmen von einem Thread zu einem anderen transportiert werden können. So steht zum Beispiel mit boost::exception_ptr
eine Klasse zur Verfügung, die std::exception_ptr
entspricht. Boost.Exception ist jedoch kein vollwertiger Ersatz für die Headerdatei exception
aus der Standardbibliothek. So fehlt Boost.Exception zum Beispiel die Unterstützung für verschachtelte Ausnahmen vom Typ std::nested_exception
.
Um die Beispiele in diesem Kapitel mit Visual C++ 2013 zu kompilieren, entfernen Sie das Schlüsselwort noexcept
. Diese Version des Microsoft-Compilers unterstützt noexcept
noch nicht.
boost::exception
in Aktion#include <boost/exception/all.hpp>
#include <exception>
#include <new>
#include <string>
#include <algorithm>
#include <limits>
#include <iostream>
typedef boost::error_info<struct tag_errmsg, std::string> errmsg_info;
struct allocation_failed : public boost::exception, public std::exception
{
const char *what() const noexcept { return "allocation failed"; }
};
char *allocate_memory(std::size_t size)
{
char *c = new (std::nothrow) char[size];
if (!c)
throw allocation_failed{};
return c;
}
char *write_lots_of_zeros()
{
try
{
char *c = allocate_memory(std::numeric_limits<std::size_t>::max());
std::fill_n(c, std::numeric_limits<std::size_t>::max(), 0);
return c;
}
catch (boost::exception &e)
{
e << errmsg_info{"writing lots of zeros failed"};
throw;
}
}
int main()
{
try
{
char *c = write_lots_of_zeros();
delete[] c;
}
catch (boost::exception &e)
{
std::cerr << boost::diagnostic_information(e);
}
}
Im Beispiel 56.1 wird in main()
eine Funktion write_lots_of_zeros()
aufgerufen, die eine Funktion allocate_memory()
aufruft. allocate_memory()
reserviert dynamisch Speicher. Die Funktion übergibt std::nothrow
an new
und überprüft den Rückgabewert von new
auf 0. Schlägt eine Speicherreservierung fehl, wird eine Ausnahme vom Typ allocation_failed
geworfen. allocation_failed
ersetzt die Ausnahme std::bad_alloc
, die standardmäßig von new
im Falle einer fehlgeschlagenen Speicherreservierung geworfen wird.
Die Funktion write_lots_of_zeros()
weist allocate_memory()
an, den größtmöglichen Speicherblock zu reservieren. Dazu wird auf max()
der Klasse std::numeric_limits
zugegriffen wird. Das Beispiel versucht absichtlich, einen so großen Speicherblock zu reservieren, dass die Reservierung fehlschlägt.
allocation_failed
ist sowohl von boost::exception
als auch von std::exception
abgeleitet. Die Ableitung von std::exception
ist für Boost.Exception nicht notwendig. So könnte allocation_failed
auch von einer Klasse aus einer anderen Klassenhierarchie abgeleitet worden sein, um sie in ein existierendes Framework einzubetten. Im Beispiel 56.1 wird auf die im Standard definierte Klassenhierarchie für Ausnahmen zugegriffen. Für Boost.Exception genügt es, wenn allocation_failed
lediglich von boost::exception
abgeleitet wäre.
Wird eine Ausnahme vom Typ allocation_failed
geworfen, kann die Funktion allocate_memory()
als Fehlerquelle identifiziert werden, da sie die einzige Funktion ist, die eine Ausnahme dieses Typs wirft. Würde allocate_memory()
in einem größeren Programm eingesetzt und von verschiedenen Funktionen aufgerufen werden, reicht allein der Typ allocation_failed
nicht aus. So wäre es für die Fehlersuche hilfreich zu wissen, welche Funktion allocate_memory()
aufgerufen hat und für die fehlgeschlagene Speicherreservierung verantwortlich ist.
Die besondere Schwierigkeit ist, dass die Ausnahme in der Funktion allocate_memory()
auftritt, allocate_memory()
aber nicht weiß, wer sie aus welchem Grund aufgerufen hat. allocate_memory()
kann die Ausnahme nicht selbst mit zusätzlichen Informationen anreichern. Dies kann nur im übergeordneten Kontext geschehen.
Mit Boost.Exception können zusätzliche Daten einer bereits geworfenen Ausnahme hinzugefügt werden. Dazu muss mit typedef
ein auf boost::error_info
basierender Typ definiert werden – und zwar für alle Daten, die neu in eine Ausnahme hineingepackt werden sollen.
boost::error_info
ist ein Template und erwartet zwei Parameter: Der erste Parameter ist ein Tag, der den neuen auf boost::error_info
basierenden Typ identifizierbar machen soll. Hier wird üblicherweise eine Struktur angegeben, die einen eindeutigen Namen bekommt. Der zweite Parameter bezieht sich auf den Typ der Daten, die in der Ausnahme gespeichert werden sollen. Im Beispiel 56.1 ist mit errmsg_info
ein neuer Typ definiert, der über die Struktur tag_errmsg
identifizierbar ist und einen String vom Typ std::string
speichern kann.
Auf errmsg_info
wird im catch
-Block der Funktion write_lots_of_zeros()
zugegriffen. Diese Funktion fängt Ausnahmen vom Typ boost::exception
ab und fügt ihnen mit Hilfe des überladenen Operators operator<<
zusätzliche Daten hinzu. Es wird ein Objekt vom Typ errmsg_info
erstellt, das mit dem String „writing lots of zeros failed“ initialisiert wird. Auf diese Weise wird diese Information in die Ausnahme gepackt, die anschließend mit throw
erneut geworfen wird.
Die Ausnahme beschreibt dank ihres Typs nicht nur eine fehlgeschlagene Speicherreservierung, sondern enthält außerdem die Information, dass die Speicherreservierung fehlschlug, als das Programm in write_lots_of_zeros()
viele Nullen schreiben wollte. Diese zusätzliche Information kann die Fehlersuche in einem größeren Program, in dem von vielen verschiedenen Funktionen auf allocate_memory()
zugegriffen wird, wesentlich erleichtern.
Um alle in der Ausnahme vorhandenen Informationen abzurufen, kann wie im Beispiel 56.1 im catch
-Block von main()
die Funktion boost::diagnostic_information()
aufgerufen werden. Diese Funktion ruft für die Ausnahme, die als Parameter übergeben wird, nicht nur what()
auf, sondern greift auch auf alle zusätzlich hinzugefügten Daten zu. boost::diagnostic_information()
gibt einen String vom Typ std::string
zurück, der zum Beispiel auf die Standardfehlerausgabe ausgegeben werden kann.
Beispiel 56.1 mit Visual C++ 2013 kompiliert gibt folgende Meldung aus:
Throw location unknown (consider using BOOST_THROW_EXCEPTION)
Dynamic exception type: struct allocation_failed
std::exception::what: allocation failed
[struct tag_errmsg *] = writing lots of zeros failed
Es wird der Typ der Ausnahme angegeben, die mit what()
erhaltene Fehlermeldung und die zusätzliche Beschreibung mit dem Namen des Tags.
Wäre die Klasse allocation_failed
nicht von std::exception
abgeleitet worden, würde boost::diagnostic_information()
nicht versuchen, die Methode what()
aufzurufen. Innerhalb der Funktion boost::diagnostic_information()
wird überprüft, ob ein Ausnahmetyp von std::exception
abgeleitet ist und what()
aufgerufen werden darf.
Der Name der Funktion, die die Ausnahme vom Typ allocation_failed
geworfen hat, fehlt in obiger Meldung. Stattdessen heißt es, dass die „throw location“ unbekannt ist.
Boost.Exception stellt ein Makro zur Verfügung, mit dem eine Ausnahme derart geworfen werden kann, dass nicht nur der Name der entsprechenden Funktion in der Ausnahme gespeichert wird, sondern zusätzliche Informationen wie der Name der Datei und die Zeilennummer.
BOOST_THROW_EXCEPTION
#include <boost/exception/all.hpp>
#include <exception>
#include <new>
#include <string>
#include <algorithm>
#include <limits>
#include <iostream>
typedef boost::error_info<struct tag_errmsg, std::string> errmsg_info;
struct allocation_failed : public std::exception
{
const char *what() const noexcept { return "allocation failed"; }
};
char *allocate_memory(std::size_t size)
{
char *c = new (std::nothrow) char[size];
if (!c)
BOOST_THROW_EXCEPTION(allocation_failed{});
return c;
}
char *write_lots_of_zeros()
{
try
{
char *c = allocate_memory(std::numeric_limits<std::size_t>::max());
std::fill_n(c, std::numeric_limits<std::size_t>::max(), 0);
return c;
}
catch (boost::exception &e)
{
e << errmsg_info{"writing lots of zeros failed"};
throw;
}
}
int main()
{
try
{
char *c = write_lots_of_zeros();
delete[] c;
}
catch (boost::exception &e)
{
std::cerr << boost::diagnostic_information(e);
}
}
Wenn anstatt von throw
das Makro BOOST_THROW_EXCEPTION
verwendet wird, wird die Ausnahme um zusätzliche Informationen wie Funktionsname, Dateiname und Zeile ergänzt. Das funktioniert jedoch nur, wenn der Compiler entsprechende Makros unterstützt. Während Makros wie __FILE__
und __LINE__
seit C++98 standardisiert sind, gibt es mit __func__
erst seit C++11 ein standardisiertes Makro, um den Namen der aktuellen Funktion zu erhalten. Da zahlreiche Compilerhersteller vor C++11 ein Makro für den Funktionsnamen definiert hatten, versucht BOOST_THROW_EXCEPTION
, den jeweils verwendeten Compiler zu erkennen und ein entsprechendes Makro zu verwenden. So gibt Beispiel 56.2 mit Visual C++ 2013 kompiliert folgende Meldung aus:
main.cpp(20): Throw in function char *__cdecl allocate_memory(unsigned int)
Dynamic exception type: class boost::exception_detail::clone_impl<struct
boost::exception_detail::error_info_injector<struct allocation_failed> >
std::exception::what: allocation failed
[struct tag_errmsg *] = writing lots of zeros failed
Im Beispiel 56.2 ist die Klasse allocation_failed
nicht mehr von boost::exception
abgeleitet. Das Makro BOOST_THROW_EXCEPTION
greift auf eine Funktion boost::enable_error_info()
zu, die erkennt, ob ein Ausnahmetyp von boost::exception
abgeleitet ist und – wenn dies nicht der Fall ist – erstellt einen neuen Ausnahmetyp, der sowohl vom angegebenen Ausnahmetyp als auch von boost::exception
abgeleitet ist. Das ist der Grund, warum obige Meldung als Ausnahmetyp nicht nur allocation_failed
ausgibt.
boost::get_error_info()
#include <boost/exception/all.hpp>
#include <exception>
#include <new>
#include <string>
#include <algorithm>
#include <limits>
#include <iostream>
typedef boost::error_info<struct tag_errmsg, std::string> errmsg_info;
struct allocation_failed : public std::exception
{
const char *what() const noexcept { return "allocation failed"; }
};
char *allocate_memory(std::size_t size)
{
char *c = new (std::nothrow) char[size];
if (!c)
BOOST_THROW_EXCEPTION(allocation_failed{});
return c;
}
char *write_lots_of_zeros()
{
try
{
char *c = allocate_memory(std::numeric_limits<std::size_t>::max());
std::fill_n(c, std::numeric_limits<std::size_t>::max(), 0);
return c;
}
catch (boost::exception &e)
{
e << errmsg_info{"writing lots of zeros failed"};
throw;
}
}
int main()
{
try
{
char *c = write_lots_of_zeros();
delete[] c;
}
catch (boost::exception &e)
{
std::cerr << *boost::get_error_info<errmsg_info>(e);
}
}
Im Beispiel 56.3 wird nicht wie zuvor boost::diagnostic_information()
verwendet, sondern mit boost::get_error_info()
gezielt auf die Fehlermeldung vom Typ errmsg_info
zugegriffen. Da boost::get_error_info()
einen Smartpointer vom Typ boost::shared_ptr
zurückgibt, muss über das Sternchen auf die Fehlermeldung zugegriffen werden. Nur für den Fall, dass der Parameter, der an boost::get_error_info()
übergeben wird, nicht vom Typ boost::exception
ist, ist der Smartpointer leer. Werfen Sie alle Ausnahmen mit dem Makro BOOST_THROW_EXCEPTION
, ist sichergestellt, dass der Ausnahmetyp von boost::exception
abgeleitet ist – eine Überprüfung des Smartpointers auf 0 ist dann nicht notwendig.