wake-up-neo.com

Wie würden Sie Alexandrescus Expected <T> mit Void-Funktionen verwenden?

Also bin ich auf diese (IMHO) sehr nette Idee gestoßen, eine zusammengesetzte Struktur aus einem Rückgabewert und einer Ausnahme zu verwenden - Expected<T>. Es überwindet viele Mängel der herkömmlichen Methoden zur Fehlerbehandlung (Ausnahmen, Fehlercodes).

Siehe den Vortrag von Andrei Alexandrescu (Systematische Fehlerbehandlung in C++) und seine Folien .

Die Ausnahmen und Fehlercodes haben grundsätzlich dieselben Verwendungsszenarien mit Funktionen, die etwas zurückgeben, und mit Funktionen, die dies nicht tun. Expected<T> scheint dagegen nur auf Funktionen ausgerichtet zu sein, die Werte zurückgeben.

Meine Fragen lauten also:

  • Hat jemand von euch Expected<T> in der Praxis ausprobiert?
  • Wie würden Sie diese Redewendung auf Funktionen anwenden, die nichts zurückgeben (dh leere Funktionen)?

Update:

Ich denke, ich sollte meine Frage klären. Die Spezialisierung Expected<void> macht Sinn, aber es interessiert mich mehr, wie sie verwendet wird - die konsequente Umgangssprache. Die Implementierung selbst ist zweitrangig (und einfach).

Zum Beispiel gibt Alexandrescu dieses Beispiel (ein bisschen bearbeitet):

string s = readline();
auto x = parseInt(s).get(); // throw on error
auto y = parseInt(s); // won’t throw
if (!y.valid()) {
    // ...
}

Dieser Code ist so "sauber", dass er auf natürliche Weise fließt. Wir brauchen den Wert - wir bekommen ihn. Mit expected<void> müsste man jedoch die zurückgegebene Variable erfassen und eine Operation ausführen (wie .throwIfError() oder so), die nicht so elegant ist. Und offensichtlich macht .get() mit void keinen Sinn.

Wie würde Ihr Code aussehen, wenn Sie eine andere Funktion hätten, z. B. toUpper(s), die die Zeichenfolge direkt ändert und keinen Rückgabewert hat?

34
Alex

Hat einer von euch es erwartet? in der Praxis?

Es ist ganz natürlich, ich habe es schon benutzt, bevor ich diesen Vortrag gesehen habe.

Wie würden Sie dieses Idiom auf Funktionen anwenden, die nichts zurückgeben (dh leere Funktionen)?

Das in den Folien dargestellte Formular hat einige subtile Auswirkungen:

  • Die Ausnahme ist an den Wert gebunden.
  • Es ist in Ordnung, die Ausnahme nach Ihren Wünschen zu behandeln.
  • Wenn der Wert aus bestimmten Gründen ignoriert wird, wird die Ausnahme unterdrückt.

Dies gilt nicht, wenn Sie expected<void> haben, da die Ausnahmebedingung immer ignoriert wird, da sich niemand für den void-Wert interessiert. Ich würde dies erzwingen, da ich das Lesen von expected<T> in der Alexandrescus-Klasse mit Assertions und einer expliziten suppress-Memberfunktion erzwingen würde. Das Zurückwerfen der Ausnahme aus dem Destruktor ist aus guten Gründen nicht zulässig, daher müssen Assertions ausgeführt werden.

template <typename T> struct expected;

#ifdef NDEBUG // no asserts
template <> class expected<void> {
  std::exception_ptr spam;
public:
  template <typename E>
  expected(E const& e) : spam(std::make_exception_ptr(e)) {}
  expected(expected&& o) : spam(std::move(o.spam)) {}
  expected() : spam() {}

  bool valid() const { return !spam; }
  void get() const { if (!valid()) std::rethrow_exception(spam); }
  void suppress() {}
};
#else // with asserts, check if return value is checked
      // if all assertions do succeed, the other code is also correct
      // note: do NOT write "assert(expected.valid());"
template <> class expected<void> {
  std::exception_ptr spam;
  mutable std::atomic_bool read; // threadsafe
public:
  template <typename E>
  expected(E const& e) : spam(std::make_exception_ptr(e)), read(false) {}
  expected(expected&& o) : spam(std::move(o.spam)), read(o.read.load()) {}
  expected() : spam(), read(false) {}

  bool valid() const { read=true; return !spam; }
  void get() const { if (!valid()) std::rethrow_exception(spam); }
  void suppress() { read=true; }

  ~expected() { assert(read); }
};
#endif

expected<void> calculate(int i)
{
  if (!i) return std::invalid_argument("i must be non-null");
  return {};
}

int main()
{
  calculate(0).suppress(); // suppressing must be explicit
  if (!calculate(1).valid())
    return 1;
  calculate(5); // assert fails
}
11
ipc

Auch wenn es für jemanden, der sich ausschließlich auf C-ish-Sprachen konzentriert, vielleicht neu erscheint, für diejenigen von uns, die einen Eindruck von Sprachen hatten, die Summentypen unterstützen, ist es nicht der Fall.

In Haskell haben Sie zum Beispiel:

data Maybe a = Nothing | Just a

data Either a b = Left a | Right b

Wo | oder liest und das erste Element (Nothing, Just, Left, Right) nur ein "Tag" ist. Im Wesentlichen sind Summenarten gerade diskriminierende Gewerkschaften .

Hier hätten Sie Expected<T> so etwas wie: Either T Exception mit einer Spezialisierung für Expected<void>, die mit Maybe Exception verwandt ist.

13
Matthieu M.

Wie Matthieu M. gesagt hat, ist dies für C++ etwas Neues, für viele Funktionssprachen jedoch nichts Neues. 

Ich möchte hier meine 2 Cents hinzufügen: Ein Teil der Schwierigkeiten und Unterschiede findet sich meiner Meinung nach im "prozeduralen vs. funktionalen" Ansatz. Und ich würde gerne Scala verwenden (weil ich sowohl mit Scala als auch mit C++ vertraut bin und ich glaube, dass es eine Möglichkeit (Option) hat, die näher an Expected<T> liegt), um diese Unterscheidung zu veranschaulichen.

In Scala haben Sie die Option [T], die entweder Some (t) oder None ist. Insbesondere ist es auch möglich, eine Option [Unit] zu haben, die moralisch äquivalent zu Expected<void> ist.

In Scala ist das Verwendungsmuster sehr ähnlich und baut auf zwei Funktionen auf: isDefined () und get (). Es hat aber auch eine "map ()" Funktion.

Ich betrachte "map" gerne als funktionelles Äquivalent von "isDefined + get":

if (opt.isDefined)
   opt.get.doSomething

wird

val res = opt.map(t => t.doSomething)

"Propagieren" der Option zum Ergebnis

Ich denke, dass hier, in diesem funktionalen Stil des Verwendens und Zusammenstellens von Optionen, die Antwort auf Ihre Frage liegt:

Wie würde also Ihr Code aussehen, wenn Sie eine andere Funktion hätten, etwa toUpper (s), die den String an Ort und Stelle ändert und keinen Rückgabewert hat?

Ich persönlich würde die Zeichenfolge NICHT ändern, oder ich werde nichts zurückgeben. Ich sehe Expected<T> als ein "funktionales" Konzept, das ein funktionierendes Muster benötigt, um gut zu funktionieren: toUpper (s) müssten entweder einen neuen String zurückgeben oder sich selbst nach der Änderung zurückgeben:

auto s = toUpper(s);
s.get(); ...

oder mit einer Scala-ähnlichen Karte

val finalS = toUpper(s).map(upperS => upperS.someOtherManipulation)

wenn Sie keinen funktionalen Weg verfolgen möchten, können Sie isDefined/valid verwenden und Ihren Code auf eine prozedurale Weise schreiben:

auto s = toUpper(s);
if (s.valid())
    ....

Wenn Sie dieser Route folgen (vielleicht, weil Sie dies tun müssen), müssen Sie einen Punkt für "Void vs. Unit" angeben: In der Vergangenheit wurde Void nicht als Typ betrachtet, sondern "Kein Typ" (Void Foo () wurde als "Pascal" betrachtet) Verfahren). Einheit (wie sie in funktionalen Sprachen verwendet wird) wird eher als Typus verstanden, der "Berechnung" bedeutet. Die Rückgabe einer Option [Unit] macht also mehr Sinn, da sie als "eine Berechnung betrachtet wird, die optional etwas getan hat". Und in Expected<void> nimmt void eine ähnliche Bedeutung an: eine Berechnung, die, wenn sie wie beabsichtigt funktioniert (wenn es keine Ausnahmefälle gibt), nur endet (nichts zurückgibt). Zumindest IMO!

Die Verwendung von Expected oder Option [Unit] kann daher als Berechnung angesehen werden, die möglicherweise zu einem Ergebnis führt oder nicht. Ihre Verkettung wird es schwierig machen:

auto c1 = doSomething(s); //do something on s, either succeed or fail
if (c1.valid()) {
   auto c2 = doSomethingElse(s); //do something on s, either succeed or fail
   if (c2.valid()) { 
        ...

Nicht sehr sauber.

Karte in Scala macht es ein bisschen sauberer

doSomething(s) //do something on s, either succeed or fail
   .map(_ => doSomethingElse(s) //do something on s, either succeed or fail
   .map(_ => ...)

Welches ist besser, aber noch lange nicht ideal. Hier gewinnt die Vielleicht-Monade eindeutig ... aber das ist eine andere Geschichte ...

5

Ich habe über die gleiche Frage nachgedacht, seit ich dieses Video gesehen habe. Und bisher fand ich kein überzeugendes Argument dafür, dass ich erwartet hatte, für mich sieht es lächerlich aus und gegen Klarheit und Reinheit. Ich habe mir bisher folgendes ausgedacht:

  • Erwartet ist gut, da es entweder Wert oder Ausnahmen hat. Wir müssen try {} catch () nicht für jede Funktion verwenden, die auslösbar ist. Verwenden Sie es also für jede Wurffunktion, die einen Rückgabewert enthält
  • Jede Funktion, die nicht geworfen wird, sollte mit noexcept gekennzeichnet sein. Jeden.
  • Jede Funktion, die nichts zurückgibt und nicht als noexcept markiert ist, sollte von try {} catch {} eingeschlossen werden.

Wenn diese Aussagen gelten, haben wir selbstdokumentierte, einfach zu bedienende Schnittstellen mit nur einem Nachteil: Wir wissen nicht, welche Ausnahmen ausgelöst werden könnten, ohne in die Implementierungsdetails zu schauen.

Erwartet erhebt einige zusätzliche Kosten für den Code. Wenn Sie eine Ausnahme im Kern Ihrer Klassenimplementierung haben (z. B. tief in privaten Methoden), sollten Sie ihn in Ihrer Schnittstellenmethode abfangen und Erwartet zurückgeben. Obwohl ich denke, dass es für die Methoden, die eine Idee haben, etwas zurückzugeben, durchaus erträglich ist, glaube ich, dass es die Methoden durcheinander bringt, die per Design keinen Rückgabewert haben. Außerdem ist es für mich ziemlich unnatürlich, etwas von etwas zurückzugeben, das nichts zurückgeben soll.

2
ixSci

Es sollte mit Compiler-Diagnose behandelt werden. Viele Compiler geben bereits eine Warnungsdiagnose aus, die auf der erwarteten Verwendung bestimmter Standard-Bibliothekskonstrukte basiert. Sie sollten eine Warnung ausgeben, um einen expected<void> zu ignorieren.

0
Omnifarious