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.