Die Boost C++ Bibliotheken

Kapitel 56. Boost.Exception

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.

Anmerkung

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.

Beispiel 56.1. 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.

Beispiel 56.2. Mehr Informationen mit 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.

Beispiel 56.3. Gezielter Zugriff mit 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.