C++ Result

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.

1class Foo {
2public:
3  int         doSomething();
4  Result<int> tryDoSomething() noexcept;
5};

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.

📧 📋 🐘 | 🔔
 

Meine Dokus über:
 
Weitere externe Links zu:
Alle extern verlinkten Webseiten stehen nicht in Zusammenhang mit opengate.at.
Für deren Inhalt wird keine Haftung übernommen.



Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!