1. Warum C++ noexcept braucht
Wenn Sie an ein gewöhnliches return gewöhnt sind, klingt „Ausstieg über Ausnahme“ wie eine alternative Realität. Und in gewissem Sinne ist es das auch: es ist ein Weg, bei dem die Funktion nicht auf normale Weise die Kontrolle zurückgibt, sondern die Ausführung „nach außen durchbricht“. Heute lernen wir nicht die Syntax von try/catch und wie man Ausnahmen „fängt“ — uns reicht das Modell: manchmal kann in einer Funktion ein Ereignis auftreten, das dazu führt, dass die Kontrolle nicht durch ein return zurückkommt.
noexcept ist eine Möglichkeit, dem Compiler und allen Lesern des Codes zu sagen: „diese Funktion darf niemals auf diese artige, absturzartige Weise enden“. Das ist kein Kommentar und keine Bitte. Es ist ein Versprechen auf Vertragsniveau. Außerdem ist in modernen C++-Versionen die Ausnahmespezifikation Teil des Funktionstyps (das heißt, sie beeinflusst Signaturkompatibilität und Überladung).
Und hier liegt der zentrale Sinn: noexcept hilft, Code vorhersehbar zu machen. Wenn Sie einen Typ mit Move-Unterstützung entwerfen, möchten Sie, dass das Verschieben „eisenhart“ ist: Pointer übertragen, Quelle auf null setzen — und das war's. Ohne Überraschungen.
Syntax von noexcept
Von außen wirkt noexcept fast zu simpel — und das ist gut. Ein Vertrag muss sofort lesbar sein.
Minimale Form:
#include <iostream>
void print_hello() noexcept {
std::cout << "Hello!\n"; // Hello!
}
Hier steht noexcept in der Funktionsdeklaration und bedeutet: diese Funktion darf nicht durch einen Ausnahmeweg beendet werden. Im Rahmen unseres vereinfachten Modells kann man heute so denken: wenn innerhalb von print_hello() etwas passiert, das versucht, „als Ausnahme nach außen zu fliegen“, wird das Programm nicht «wie gewohnt» weiterlaufen.
Noch ein wichtiger Punkt: noexcept ist kein „Beschleuniger“. Ja, es kann Optimierungen beeinflussen, aber in erster Linie ist es eine Semantik. Ein Vertrag. Ein Versprechen.
Man sieht auch manchmal die Form noexcept(true) oder noexcept(false). Im Moment reicht es, die Idee zu verstehen: noexcept kann bedingt sein, aber in unseren Beispielen verwenden wir die grundlegende Form noexcept, weil sie sich am besten für Move-Operationen von ressourcenhaltenden Typen eignet.
2. Was passiert bei Verletzung von noexcept
Stellen Sie sich vor, noexcept ist ein Schild „NICHT EINTRETEN“ an einer Tür. Solange Sie es respektieren — alles in Ordnung. Wenn Sie es jedoch ignorieren und eintreten, ist die Folge kein Bußgeld, sondern „die Tür schließt sich hinter Ihnen und das Gebäude geht in den Evakuierungsmodus“.
Technisch ausgedrückt: wenn eine Ausnahme versucht, eine noexcept-Funktion zu verlassen, wird std::terminate() aufgerufen und das Programm beendet sich abrupt. Das ist kein „kann passieren“, das ist das Sprachmodell.
Zur Veranschaulichung ist ein Flussdiagramm hilfreich:
flowchart TD
A["Funktion ist noexcept deklariert"] --> B["Innerhalb tritt ein Ereignis auf, das als Ausnahme nach außen möchte"]
B --> C{"Verlässt die Ausnahme die noexcept-Funktion?"}
C -- "Ja" --> D["std::terminate()"]
D --> E["Programm beendet sich abrupt"]
C -- "Nein" --> F["Ausführung geht normal weiter"]
Warum so radikal? Weil noexcept ein Versprechen ist, auf das sich andere Programmteile und die Standardbibliothek verlassen können. Würde eine Verletzung von noexcept „sanft“ gehandhabt, würde der gesamte Vertragswert erodieren.
Deshalb gilt die wichtige Regel für Einsteiger: setzen Sie niemals noexcept „auf Verdacht“. Das ist kein „Kann man machen“, sondern ein „Ich bin mir sicher, dass nichts nach außen fliegen wird“.
4. Warum noexcept für Move-Operationen wichtig ist
Hier fragen viele: „Ok, der Vertrag ist streng. Aber warum gibt es so viel Aufhebens um noexcept beim Move?“ Die Antwort ist praktisch: Verschieben wird oft innerhalb der Standardcontainer verwendet, insbesondere wenn ein Container Elemente an einen neuen Ort „umsiedeln“ muss.
Denken Sie an std::vector<T>. Er speichert Elemente in einem zusammenhängenden Speicherblock. Manchmal muss er die Kapazität erhöhen, einen neuen Block allokieren und die Elemente dorthin verschieben. Dann stellt sich die Frage: soll er per Kopie oder per Move verschieben?
Wenn der Move-Konstruktor des Typs T mit noexcept gekennzeichnet ist, erscheint das Verschieben für vector als „sichere Umsiedlung“. Wenn der Move jedoch potenziell eine Ausnahme werfen kann, bevorzugt der Container möglicherweise das Kopieren (falls Kopieren verfügbar ist). In der Standardbibliothek existiert sogar die Idee „move only if it does not throw“ — oft assoziiert mit move_if_noexcept (der Name hilft als Merkstütze, die Motivation zu verstehen).
Das bedeutet nicht, dass ohne noexcept alles zusammenbricht. Es bedeutet, dass der Typ für Container und Algorithmen, die sichere und effiziente Umsiedlungen wollen, weniger freundlich wird.
5. Beispiel: ressourcenbesitzender Buffer und Move‑Operationen mit noexcept
Schauen wir uns noexcept an einem konkreten Typ an, der direkt eine Ressource besitzt.
Angenommen, wir haben einen Buffer — einen Besitzer eines dynamischen Arrays von int. Das ist ein Lernbeispiel, zeigt aber gut das Ownership‑Prinzip:
#include <cstddef>
struct Buffer {
std::size_t size{};
int* data{nullptr};
explicit Buffer(std::size_t n) : size(n), data(new int[n]{}) {}
~Buffer() { delete[] data; }
};
Das bereits bekannte Problem: das Standard-Kopieren bricht Ownership (es kopiert die Adresse). Daher haben wir in früheren Lektionen tiefe Kopien (Copy-Konstruktor und Copy-Assignment) ergänzt. Jetzt fügen wir das Verschieben hinzu — und genau hier ist noexcept meist angebracht.
Move-Konstruktor und noexcept
Wichtig ist die Bedeutung: Der Move-Konstruktor führt typischerweise sehr einfache Aktionen aus: Pointer und Größe übernehmen, Quelle nullen. In solchem Code wird normalerweise kein Speicher mehr alloziert und es gibt keine „gefährlichen“ Operationen. Deshalb ist es sinnvoll und logisch, ihn mit noexcept zu deklarieren: man kann ehrlich versprechen, dass kein „Herausfliegen“ passiert.
#include <cstddef>
struct Buffer {
std::size_t size{};
int* data{nullptr};
explicit Buffer(std::size_t n) : size(n), data(new int[n]{}) {}
~Buffer() { delete[] data; }
Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
other.size = 0;
other.data = nullptr;
}
};
Beachten Sie die zwei „heiligen“ Aktionen: wir übernehmen die Ressource und bringen die Quelle in einen leeren Zustand. Dieser leere Zustand übersteht den Destruktor korrekt (delete[] von nullptr ist sicher).
Move-Assign und noexcept
Die Logik ist ähnlich, aber es gibt zusätzliche Verantwortung: links könnte bereits eine Ressource liegen, die sauber freigegeben werden muss. Auch hier gibt es üblicherweise keinen Grund für ein „Herausfliegen“, sofern man nichts anderes als delete[] und Zuweisungen macht.
#include <cstddef>
struct Buffer {
std::size_t size{};
int* data{nullptr};
explicit Buffer(std::size_t n) : size(n), data(new int[n]{}) {}
~Buffer() { delete[] data; }
Buffer(Buffer&& other) noexcept : size(other.size), data(other.data) {
other.size = 0;
other.data = nullptr;
}
Buffer& operator=(Buffer&& other) noexcept {
if (this == &other) return *this;
delete[] data;
size = other.size;
data = other.data;
other.size = 0;
other.data = nullptr;
return *this;
}
};
Kleine „Gesundenverstand-Prüfung\": wenn Sie in Move-Operationen plötzlich new verwenden, dann machen Sie höchstwahrscheinlich etwas falsch. Move ist „Übertragung von Ownership“, nicht „Erzeugung einer neuen Ressource“.
6. Wie man beurteilt, ob man noexcept schreiben kann
Einsteiger wünschen sich oft ein universelles Rezept: „Sag mir, wann ja und wann nein“. Ein 100%iges Universalrezept gibt es nicht. Aber es gibt eine sehr brauchbare Prüflogik, die man auch ohne tiefes try/catch-Wissen anwenden kann.
Stellen Sie sich zuerst die Frage: „Kann diese Funktion etwas tun, das potenziell versucht, als Ausnahme nach außen zu gehen?“ Typische Verdächtige sind Allokationen, Erzeugung komplexer Objekte, I/O-Operationen, „schlaue“ Container, die intern Speicher allokieren können. Wenn die Antwort „ja“ oder „unsicher“ ist — setzen Sie kein noexcept.
Wenn die Funktion nur einfache, vorhersehbare Aktionen macht wie Pointer-Zuweisungen, Vertauschung ganzer Zahlen, Nullsetzen von Feldern oder Freigabe einer Ressource, dann ist noexcept in der Regel angemessen.
Eine nützliche Mini-Tabelle:
| Operation | Oft angemessen: noexcept? | Warum |
|---|---|---|
| Move „Pointer übertragen + Nullsetzen“ | Ja | Normalerweise keine Speicherallokation |
| Destruktor, der einfach eine Ressource freigibt | Ja | Ressourcenfreigabe sollte zuverlässig sein |
| Kopieren mit new und Kopierschleife | In der Regel nein | Speicherallokation kann fehlschlagen |
| Funktion, die in std::cout schreibt | In der Regel nein | I/O kann Exceptions auslösen, und besser kein hartes Versprechen |
noexcept ist Teil der Schnittstelle. Sie sagen nicht nur „intern ist alles ok“, sondern Sie sagen dem Nutzer Ihres Typs: „auf mich kann man bauen“. Daher ist es besser, nicht zu viel zu versprechen, als zu viel zu versprechen.
Warum noexcept etwas mit Vertrauen zu tun hat
In der Programmierung dienen „Versprechen“ nicht der Moral, sondern dazu, dass andere Systemteile Entscheidungen treffen können. Und noexcept ist genau so ein Versprechen. Es ermöglicht es, einfacheren und vorhersehbareren Code um Ihre Funktion herum zu schreiben.
Zum Beispiel: wenn Ihr Typ Move-Operationen mit noexcept hat, kann das Verschieben als Baustein für sichere Umstellungen verwendet werden. Auch wenn Sie das aktuell nicht direkt nutzen, kann die Standardbibliothek es nutzen.
Außerdem diszipliniert noexcept Sie als Autor: wenn Sie Move mit noexcept markieren, setzen Sie sich selbst die Regel: „im Move füge ich keine Logik hinzu, die zu Problemen führen kann“. Das führt häufig zu saubererem Design: Move bleibt ein mechanischer Ownership-Transfer, und komplexe Arbeit landet in normalen Funktionen oder im Kopieren, falls nötig.
7. Typische Fehler im Umgang mit noexcept
Fehler Nr. 1: noexcept setzen, weil „das so üblich ist“.
Die häufigste Falle ist, irgendwo im Internet Buffer(Buffer&&) noexcept zu sehen und das blind zu kopieren, ohne den Preis des Versprechens zu verstehen. Wenn es in der Funktion irgendein Szenario gibt, das einen „Ausnahmeweg“ ermöglichen könnte, unterschreiben Sie einen Vertrag, dessen Verletzung das Programm via std::terminate() beendet.
Fehler Nr. 2: noexcept markieren bei Funktionen, die Speicher allokieren.
Ein Einsteiger denkt manchmal: „aber normalerweise klappt die Allokation doch“. „Normalerweise“ ist kein Vertrag. Allokation ist wie Parkplatzsuche an Silvester: manchmal klappt es, manchmal nicht. Deshalb sollte Kopieren, das new verwendet, fast nie mit noexcept versehen werden.
Fehler Nr. 3: noexcept und const verwechseln.
Diese Wörter stehen oft nebeneinander in Deklarationen, und das Gehirn will sie verschmelzen. const bedeutet „Methode verändert nicht den Objektzustand“. noexcept bedeutet „Funktion beendet sich nicht durch einen Ausnahmeweg“. Das sind unabhängige Achsen: eine betrifft Objektveränderung, die andere Ausnahmepfade.
Fehler Nr. 4: Move-Operationen verkomplizieren und vergessen, noexcept zu überprüfen.
Typischer Ablauf: zuerst ist Move nur Pointer-Transfer und ehrlich mit noexcept. Dann fügt jemand Logging hinzu, Hash-Neuberechnung, temporäre std::string-Erzeugung, und Move ist nicht mehr trivial. Dennoch bleibt noexcept stehen. Ergebnis: eine Zeitbombe — es sieht gut aus, aber plötzlich beendet sich das Programm unerwartet.
Fehler Nr. 5: denken, noexcept sei nur Optimierung und man müsse sich nicht kümmern.
Ja, noexcept kann Verhalten und Effizienz von Containern verbessern. Aber sein Hauptzweck ist die Korrektheit des Vertrags. Es ist nicht Motortuning, es sind die Bremsen. Optimierung ist ein angenehmer Nebeneffekt, aber nicht der Grund, noexcept zu setzen.
GO TO FULL VERSION