C++ Result
«
| 09 Jul 2023 |
»
Es gibt da diese eine Sache, die von Rust und ZigLang herüberzieht, die mir keine Ruhe mehr lässt: Nämlich die Rückgabe von Ergebnissen und Fehlern in einer eigenen Datenstruktur ohne Exceptions.
Tja … und da C++23 mit std::expected
einfach noch zu weit weg ist,
habe ich kurzerhand Result<T, E>
in mein Framework aufgenommen.
Das Problem mit C++ Exceptions ist nicht das Konzept, sondern dessen Implementierungen in GCC und MSVC, die immer nur eine Ausprägung zulassen. Man könnte Exception auf unterschiedliche Weisen in Binärcode abbilden, doch um modul-übergreifend kompatibel zu sein, muss man bei einer Variante bleiben.
Und somit verhindern C++ Exceptions weiter, dass ich “lesbaren” Code für EFI-Applications schreiben kann, weil ich nur reine C Routinen dort verarbeiten kann.
Andere (EFI-) Projekte haben das auch durchgemacht und propagieren C++
ohne Exceptions und RTTI
, womit sie erfolgreich sind.
Doch wie werden dort Fehler behandelt?
Return-value Codes
Tja, man brauch Return-Typen, die sowohl Fehler als auch Erfolgsresultate erhalten können. Rust und ZIG tragen dieses Konzept in der Kernsprache und können es dort (hoffentlich) gut optimieren.
In C++ kann man natürlich ein Template schreiben, dass wie ein std::variant
einen von zwei möglichen Typen enthalten kann.
Ein solcher std::expected<succes_type, error_type>
Typ wird entweder mit
dem einen oder anderen Typen konstruiert und gibt ihn so gekapselt an den
Aufrufer zurück.
Dieser kann dann aus-if-en, ob er einen Fehler oder einen Erfolg erhalten hat.
Ein Code sagt mehr als 1000 Worte:
1 2struct division_error {}; 3 4int devide_classic(int a, int b) { 5 if(b == 0) { 6 throw division_error(); 7 } 8 return a / b; 9} 10 11void do_devide_classic() { 12 try { 13 int result = devide_classic(4, 2); 14 std::cout << "Result=" << result << std::endl; 15 } 16 catch(devision_error const&) { 17 std::cout << "Error" << std::endl; 18 } 19} 20 21std::expected<int, division_error> devide_new(int a, int b) { 22 if(b == 0) { 23 return division_error(); 24 } 25 return a / b; 26} 27 28void do_devide_new() { 29 auto result = devide_new(4, 2); 30 if(result) { 31 std::cout << "Result=" << *result << std::endl; 32 } 33 else { 34 std::cout << "Error" << std::endl; 35 } 36}
Umsetzung im GATE Projekt
Eigentlich sind Exception ein zentraler Bestandteil der C++ Implementierung des GATE Projektes. Ich kann jetzt nicht auf ein anderes Error-Modell umsteigen ohne alles neu schreiben zu müssen.
Allerdings möchte ich schrittweise die bestehenden C++ Klassen erweitern.
Neben den normalen Methoden mit Exception-Support, soll es alternative
Funktionen mit dem Präfix try
geben die ein gate::Result<T, E>
zurückliefern
und eben keine Exceptions auslösen.
Meine Hoffnung liegt beim C++ Optimizer, der die als statische Bibliotheken erzeugten C++ Klassen scannt und dann nur die Methoden übersetzt, die tatsächlich benutzt werden.
So kann man Apps schreiben, die nur die Exception-freien C++ Methoden nutzen.
Fazit
Andere Länder - andere Sitten.
Andere Sprachen - andere Fehlerbehandlungen.
Dieser Ansatz ist ungewöhnlich und ich bin mir noch nicht sicher, was ich am Ende davon halten soll. Trotzdem denke ich, dass Exception-frei Apps eine Existenzberechtigung haben und somit werde ich mal sehen, wo mich dieser Weg hinführt.