C++ im Wandel

(haha, vergifteter gehts nicht mehr)
Jo. Gerade heute wieder einen Kommentar auf eine Antwort gelesen:
upload_2018-4-21_21-54-22.png

Krampfhafte Patternisierung ist böse
Mhm, hatte erst gerade kürzlich die Pattern in einer Vorlesung. Da wurde z.B. ernsthaft gesagt, man solle das Factory-Pattern benutzen, um einfacheres Refactoring zu erlauben. Nur in einem einzigen Fall finde ich das zu bevorzugen: Wenn idiomatisch ein ::create() benutzt werden kann, das einige immergleiche Parameter des Basisklassenkonstruktors bindet.
(Manche Pattern, wie das Visitor-Pattern bei ASTs, sind für einige wenige Anwendungsbereiche extrem gut geeignet).

Aber selbst Wikipedia schreibt korrekt:
Der erfolgreiche Einsatz von Entwurfsmustern in der Vergangenheit kann dazu verleiten, die Entwurfsmuster als Wunderwaffe und Garant für gutes Design anzusehen. Unerfahrene Entwickler können geneigt sein, möglichst viele bekannte Muster zu verwenden, und dabei übersehen, dass in ihrem Fall vielleicht eine elegantere Lösung ohne den Einsatz von Mustern möglich wäre. Entwurfsmuster garantieren nicht, dass der Entwurf gut ist. Insofern ist die Anwendung zu vieler oder ungeeigneter Entwurfsmuster ein Antimuster.
Nur kommt das in den Vorlesungen nicht so rüber. Es heisst schlicht, Patterns seien das einzig Wahre, dann bekommt man in einer Prüfung irgendeinen 100-Zeilen-Code vorgesetzt und muss dann sagen, dass es schlecht ist, new String statt StringBuilderFactory().build().make() zu benutzen, weil letzeres ja so viel erweiterbarer sein soll.
Das sind die Professoren, die ihre Semantischen Analyzer auch ohne Overflow arbeiten lassen, weil es ja so viel Sinn macht, einen möglichen Resultatbereich eines Integers von [0, \inf[ zu haben :confused:
Ein RestAPI-Ding, bei dem der einfachste Request 20sec braucht.
Hui, wird da Pi in die tausendste Stelle berechnet oder 1GB Daten durchsucht?

(Die Exception für falsche Keys hört sich für mich gar nicht so schlimm an...)
Das ist zwar fast wieder eine andere Diskussion, aber wieso wären hier Returnvalues nicht zu bevorzugen (die meisten Funktionen von CryptoPP geben ohnehin void zurück)? Das Problem mit Exceptions ist ja gerade, dass sie irgendwann mal mitten in einem Code, der Libraryfunktionen benutzt, auftreten können. Catchen muss man die Exceptions ohnehin möglichst nahe am throw, sonst bekommt man sehr schnell einen Inconsistent State. Und ähnlich wie mit new, dessen throw ich wirklich fürchte, welcher eigentlich nicht nötig ist, da man auch auf nullptr prüfen kann, sehe ich keinen Grund für Exceptions, da man ja einfach einen bool/bitset/errnum zurückgeben könnte, der dann halt zu prüfen ist.
Ich weiss schon, dass u.a. bei Java die Exceptions meistens recoverable sind, und ich sehe auch das Argument, dass es eher unerwartet ist, dass Fehler geschehen (eine sehr optimistische Einstellung). Aber warum wird dann eine Exception so lange automatisch rethrowt, bis es jemand handelt, und zerschiesst dabei alle Stacks davor? Warum wird nicht erzwungen, dass Exceptions nur eine Stufe durchdringen können, um es überschaubar zu halten? Bei einem Retry würde dann ja immer alles komplett neu aufgebaut, was auch wieder kostet. Und wenn etwas wirklich kaputtgehen kann, dann müsste das ja schon im Vorherein klar sein, und man könnte ein Callback setzen.

dass man mehr als drei verschachtelte Level von Schleifen/if/... in der Realität nie sehen wird :D lol
Wobei das meines Wissens im Styleguide für Kernelcode ist. Wie sehr das durchgesetzt wird, weiss ich aber auch nicht, und i.d.R. sind auf den tieferen Stufen auch weniger Unterscheidungen nötig.

Auf die Frage, was man bei Funktionen mit 20 Parametern (bei der man die Parameterliste nicht ädnern kann!) und gleich am Anfang 200 Zeilen Validierung der Parameter, bevor es zum eigentlichen Sinn der Funktion kommt, tun soll, konnte mir bisher niemand von solchen Leuten eine überzeugende Antwort geben.
20 Setter :)
Immer beliebt ist der Spruch: "Wer 11 Parameter hat, hat vermutlich einen vergessen." Ja ne is klar. Das sind dann aber auch die Vögel, die 300 Werte in ein JSON-Objekt schreiben und dann 2 davon benutzen. Sind ja keine Parameter oder so. Oder sie machen es viel klüger und erstellen einzig für die Verwaltung der Parameter ein Objekt, selbstverständlich auf dem Heap, damit der ach so lahme Stack nicht benutzt werden muss. Beim ersten WinAPI-Code würden die Leute ja einen Schreikrampf kriegen...
Ich meine, keiner mag viele Parameter, aber manchmal müssen sie hin und dann sieht es halt nicht sehr gut aus, aber meine Güte, diese Funktionen werden i.d.R. nicht als Interface gegeben, sondern intern benutzt.

Doch, so oder so ähnlich machen das recht viele Leute.

...Erinnert mich an einen Kollegen, der tatsächlich jedes if-foo-then-error-4 in zwei (!) Klassen ausgelagert hat (eine für das if, eine mit den Fehlerdaten). Könnte ja wiederverwendbar sein. War es teilweise auch, aber natürlich keine Nettoersparnis an Zeilen und Arbeitszeit. Inzwischen hat sogar er eingesehen, dass es Unsinn ist.
Ah ja, Wiederverwendbarkeit. Eigentlich nichts schlechtes, aber die Leute rollen immer erst die Strukturen aus und machen dann den eigentlichen Arbeitscode, sodass immer etwa 3 Wrapperklassen sinnlos vor sich hindümpeln. Sieht halt schon viel besser aus, wenn man keinen Code sieht :rolleyes: Ich halte es grundsätzlich eher konservativ: Solange es lesbar ist, ist alles ok. Wird es zu komplex, wird ausgelagert.

Hast du auch mal einen Grund bekommen? Also einen außer "ich mag es nicht" und/oder "RandomPersonImInternet mag es nicht"?
"Wenn eine Funktion so viele Zeilen hat, macht sie zu viel gleichzeitig" (auch auf Klassen anwendbar, halt mit Member etc.).
Und es ist wohl wahr, ich lagere nur dann aus, wenn es wirklich der Lesbarkeit hilft und Performance nicht allzu wichtig ist oder wenn ich dieselbe Funktionalität mehrmals brauche. Aber bei langen if-elses mit klarer Sortierung; warum sollte ich da gross auslagern? Dass langer Code per se schlecht sein soll, ist auch eine Folge von missverstandenem OOP (wie so vieles), aber leider hält sich das ja hartnäckig.

Erinner sie doch bitte mal, dass wir nicht Modedesigner sind, sondern was praxistaugliches machen wollen.
Das ist, denke ich, eines der Probleme: Man schreibt immer nur Samplecode an den Bildungseinrichtungen, nie richtigen Code. Will man irgendwas komplexes machen, nimmt man dann die einfachste Bibliothek und fertig; man muss wenig Code schreiben. Entsprechend wird auch eher die Form statt die Funktion bewertet, und das zieht sich halt so durch.
Letztens über Swift gelesen: Hat ja so geile Features wie return type overloads. Ja, man kann es sich denken:
Code:
let a: Double = -(1 + 2) + -(3 + 4) + -(5)
Das gibt allen Ernstes den Output "error: Expression too complex". Warum? https://www.cocoawithlove.com/blog/...es.html#errors-compiling-otherwise-valid-code
In Kurzform: Durch die overloaded return types gibt es exponentielle Type-Inference-Komplexität. Aber hey: Immerhin zahlt man nur mit 20s compiletime bei solch einfachen Expressions für dieses tolle Feature, das keiner je vermisste. Und: Es sieht halt besser aus, da spielt Performance keine Rolle.

Aber ich bin ja auch der Meinung, man dürfe
Code:
//Für kurze statements (~< 10 LOC)
if () {
    stmt();
}

//Für "normale" statements
if()
{
    stmt();
}

//Für 1 LOC statements
if()
    stmt();
frei mischen und die Lesbarkeit ändere sich nicht wesentlich (oder verbessert sich sogar), aber viele beharren auf einem einheitlichen Stil. Das artet auch schnell darin aus, dass manche TABs verbieten wollen oder ein Leerzeichen nach "//" verlangen, aber ich verstehe auch, wenn jemand homogenen Code haben will. Wobei dann auch einfach pretty-print angeschmissen werden könnte...
du bist nicht allein :)
Das war sicherlich eine Übertreibung, aber diese Meinung ist sicher in der Minderheit (oder die anderen sind einfach lauter, kann ja auch sein).

Eieiei, was für ein Rant. Aber muss ja auch ab und zu sein.

Gruss
cwriter
 
Aber warum wird dann eine Exception so lange automatisch rethrowt, bis es jemand handelt, und zerschiesst dabei alle Stacks davor? Warum wird nicht erzwungen, dass Exceptions nur eine Stufe durchdringen können, um es überschaubar zu halten? Bei einem Retry würde dann ja immer alles komplett neu aufgebaut, was auch wieder kostet. Und wenn etwas wirklich kaputtgehen kann, dann müsste das ja schon im Vorherein klar sein, und man könnte ein Callback setzen.
Gefühlt gibt es zwei Arten von Exceptions in einem gegebenen Kontext (dort wo sie auftreten):
  • Die, die recht nah behandelt werden. Wobei "nah" je nach Vorhandensein von Bibliotheken, Hilfsfunktionen/-klassen etc. variieren kann.
  • Die, die recht weit außen behandelt werden, weil sie fundamentale Fehler darstellen, z. B. eine NullPointerException, die zum Protokollieren des Fehlers, Anzeige einer Fehlermeldung und dann Termination der Applikation führt. Weit außen ist hier relativ zu sehen, das kann die main()-Funktion sein, ein Einstiegspunkt eines Threads oder auch die äußerste Methode einer Sandbox/Pluginumgebung. Beim letzten Fall hat ein externes Plugin zum Fehler geführt.
IMO ist das Problem, dass der Übergang recht fließend sein kann. Dementsprechend lässt du dir Flexibilität beim Exception-Modell. Im Gegensatz dazu kann man das auch einschränken zu Checked und Unchecked Exceptions, siehe Java. Das erhöht auf erste Sicht erst einmal massig den Boilerplate-Code, an sich finde ich solch eine Trennung auf Typebene gar nicht mal so schlecht. Vielleicht könnte man beim Aufruf einer Funktion gleich sagen, für welche Exceptions man sich interessiert:
Java:
try {
  File file = openFile("non-existent.txt") with fatal FileNotFoundException, caught MissingPermissionException
}
catch (MissingPermissionException e) {
  // Dass diese hier abgefangen wird, erzwingt der Compiler wegen obiger Zeile
}

Ist das Mitschleifen eines Callbackpointers auf eine ganz außen definierten Callbackfunktion nicht dasselbe wie das Erlauben der zweiten Art von Exceptions? Konzeptionell vollziehen sie beide dasselbe.

Das Problem mit Exceptions ist ja gerade, dass sie irgendwann mal mitten in einem Code, der Libraryfunktionen benutzt, auftreten können. Catchen muss man die Exceptions ohnehin möglichst nahe am throw, sonst bekommt man sehr schnell einen Inconsistent State.
Das hängt stark davon ab, ob man mit der Außenwelt, sei es Hardware oder ein externes Programm, agiert.

Da C++ durchaus als High-Level-Sprache benutzt werden kann, sind solche Flexibilitäten wie bei Exceptions imo unumgänglich für die Sprachdesigner. Aber ja, für OS- oder hardwarenahe Programmierung möchte man darauf und auf die damit verbundenen Zusatzkosten eher verzichten.
 
Die, die recht weit außen behandelt werden, weil sie fundamentale Fehler darstellen, z. B. eine NullPointerException, die zum Protokollieren des Fehlers, Anzeige einer Fehlermeldung und dann Termination der Applikation führt.
NullPointerExceptions sind aber doch ein Argument gegen Exceptions: Warum kann man nicht einfach prüfen, dass ein Wert nicht null ist, bevor man ihn verwendet? Oder mittels Contracts/Asserts verlangen, welche Vorgaben erfüllt sein müssen? Mit NullPointerExceptions sieht man ja nicht, wo die Ursache des Problems ist, sondern nur, wo die Symptome auftreten.
Weit außen ist hier relativ zu sehen, das kann die main()-Funktion sein, ein Einstiegspunkt eines Threads oder auch die äußerste Methode einer Sandbox/Pluginumgebung. Beim letzten Fall hat ein externes Plugin zum Fehler geführt.
Bei wirklich schlimmen Fehlern macht das Sinn, aber eine Funktion, die z.B. ein Netzwerkpaket erwartet und bei Timeout throwt, verstehe ich schlicht nicht. Warum nicht einfach die Anzahl gelesener Bytes zurückgeben, die dann halt 0 oder -1 sind? Warum braucht man eine Exception, die bis zur main() durchgereicht wird, wo dann ein String dargestellt wird? Ermuntert das nicht dazu, gar keine Exception mehr zu fangen und bei Problemen einfach gar nichts zu machen, da der Nutzer ja eine Nachricht bekommt? Und da z.B. Timeouts nicht allzu selten sind, warum macht man sich die Mühe von Objekten mit Strings, wo ein Integer den Fehler genausogut beschreiben kann?
Die, die recht nah behandelt werden. Wobei "nah" je nach Vorhandensein von Bibliotheken, Hilfsfunktionen/-klassen etc. variieren kann.
Aber diese Art von Fehler ginge ja recht gut mit einem Rückgabewert.
Ob man jetzt
C:
FILE* f = fopen("test.txt", "r");
if(f == NULL) {
    //"Exception" handler
}

fclose(f);
oder
Java:
try {
  File file = openFile("non-existent.txt");
}
catch (Exception e) {
   //Exception handler
}
schreibt, kommt ja echt auf dasselbe raus. Also wieso verwendet man Exceptions für eine Stufe?

IMO ist das Problem, dass der Übergang recht fließend sein kann. Dementsprechend lässt du dir Flexibilität beim Exception-Modell.
Flexibilität ist immer Fluch und Segen. Wer erfahren ist, kann sehr interessante Konstrukte bauen. Wer es nicht ist, vertut sich aufgrund mangelnder Regeln schnell.
Im Gegensatz dazu kann man das auch einschränken zu Checked und Unchecked Exceptions, siehe Java. Das erhöht auf erste Sicht erst einmal massig den Boilerplate-Code
Ich finde das einen der schlimmsten Fehler von Java. Seien wir ehrlich: Die allermeisten Leute lassen die IDE die nötigen Stubs erstellen und belassen es dabei. Man gewinnt nichts, hat mehr Code, und man verwirrt die Leute, warum einige Exceptions anders sind. C# hat checked exceptions ja auch entfernt. Hier ist wieder die Flexibilität (oder eher, dass man nicht wusste, was man wollte) ein Problem. Entweder man sagt, Exceptions sind schwere Fehler, die aber Recoverable sind und gefangen werden müssen, oder man sagt, Exceptions sind eine andere Form von Rückgabepfaden. Letzteres wäre aber Redundant mit Returnvalues (gut, syntaktisch vielleicht ein bisschen aufgeräumter), ersteres wird nicht gemacht, weil es schnell viel zu verbose würde.

Vielleicht könnte man beim Aufruf einer Funktion gleich sagen, für welche Exceptions man sich interessiert:
Das wäre sicherlich ein Ansatz.

Ist das Mitschleifen eines Callbackpointers auf eine ganz außen definierten Callbackfunktion nicht dasselbe wie das Erlauben der zweiten Art von Exceptions? Konzeptionell vollziehen sie beide dasselbe.
Callbacks würden das Stack-unwinding verhindern. Vielleicht wäre es besser, eine Strategie zu übergeben, wie z.B. bei Netzwerktimeouts eines aus {resend, keep_waiting, return} oder so.

Da C++ durchaus als High-Level-Sprache benutzt werden kann, sind solche Flexibilitäten wie bei Exceptions imo unumgänglich für die Sprachdesigner.
Nur, weil die meisten High-Level Languages Exceptions bieten, heisst das nicht, dass es eine gute Idee ist.
Exceptions sind ja in erster Linie ein Hack für Bequemlichkeit.
Z.B.:
C++:
void f()
{
    if(!g())
    {
        //Handle this error
    }
    if(!h())
    {
        //Handle this error and revert what g() did if necessary
    }
}
//Wird zu:
void f() throws
{
    g();
    h();
}
Soweit, so gut. Aber was wurde vergessen? Genau, "Revert what g() did". Bis heute gibt es keine klare Lösung für dieses Problem. Oft wird dann einfach ein check eingeführt, der zuerst prüft, ob weder g noch h fehlschlagen, und entsprechend abbrechen kann. Das wiederum funktioniert aber nur, wenn weder g noch h aus anderen Gründen fehlschlagen, die ein Check nicht zuverlässig prüfen kann, z.B. out-of-memory-errors. Und genau hier giessen Exceptions Öl ins Feuer: Die erste Variante geht nicht mehr, da durch eine Exception in einer Funktion der Rückgabewert umgangen wird, d.h. man muss dann Catchen. Aber damit hat man den Code ja wieder zu einem If-Else gemacht, das einfach stattdessen try-catch heisst:
C++:
void f()
{
    try { g(); }
    catch (...) {
        //Handle this error
    }
    try { h(); }
    catch(...)  {
        //Handle this error and revert what g() did if necessary
    }
}
Damit hat man also durch Exceptions genau gar nichts gewonnen und viele sich Nachteile wie Overhead und Runtimeunterstützungsnotwendigkeit eingebrockt.
Eine echte Lösung wäre etwas wie Transactions:
C++:
void g()
{
:onabort = {
    //Auto-Revert
}
}

transactional void f() //If unrecoverable errors occur, call onabort for all previously executed functions in that scope
{
    g();
    h(); //h() schlägt fehl -> g():onabort()
}
Damit wäre sichergestellt, dass nie ein inconsistent State erreicht wird, es ist idiomatisch (etwas ähnliches lässt sich mit RAII und einem Flag, ob bei Destruktoraufruf committed oder aborted werden soll, schon erreichen) und man hat sauberen Code. Also ist das eigentlich das, was Exceptions sein wollten, mit dem Unterschied, dass diese onabort in vielen Fällen vom Compiler automatisch generiert werden könnten (sofern jede in der Funktion aufgerufene atomare Operation auch ein onabort hat, der Rest folgt durch Induktion).

Aber ja, für OS- oder hardwarenahe Programmierung möchte man darauf und auf die damit verbundenen Zusatzkosten eher verzichten.
Wie ich hoffentlich nun klarer ausgedrückt habe, sehe ich das Problem mit Exceptions nicht nur in der Performance, sondern auch in Redundanz, Inkonsistenz und Einladung zu schlechtem Code.

Gruss
cwriter
 
Warum kann man nicht einfach prüfen, dass ein Wert nicht null ist, bevor man ihn verwendet?
Kannst du. Und was machst du, wenn er NULL ist? Fehlerausgabe auf stderr und exit(0)? Eine Exception werfen?
Assertions sind eine sinnvolle Ergänzung zu Exceptions: Assertions innerhalb einer Library, Exceptions an der Schnittstelle. Assertions stellen für mich eher das Checken der Korrektheit innerhalb einer Library dar, sie behandeln Fälle, die nie auftreten dürfen, aus rein logischen Argumenten. Im Idealfall könnte man beweisen, dass Assertions innerhalb einer Library nie ausgelöst werden. Fehlerhafte Schnittstellenbenutzung kann sehr wohl auftreten und der Anwender muss darüber informiert werden und muss die Exception ggf. abfangen können.
Zugegeben: eine AssertionException kann man auch abfangen und je nach Programmiersprache kann man auch den zu werfenden Ausdruck angeben, sodass die Linie zwischen Assertion und Exception verschwimmt.

Bei wirklich schlimmen Fehlern macht das Sinn, aber eine Funktion, die z.B. ein Netzwerkpaket erwartet und bei Timeout throwt, verstehe ich schlicht nicht.
Da gebe ich dir Recht. Schande auf mein Haupt! Etwas Ähnliches habe ich erst letztens verbrochen: meine Queue<T>#poll()-Methode wirft eine Exception, wenn kein Element verfügbar ist. Der Hintergrund ist, dass man jeden anderen Wert u. U. nicht vom generischen Parameter T (welcher auch null oder undefined sein kann in TypeScript) unterscheiden könnte. Die richtige Lösung wäre es, immer ein Optional<T> zurückzugeben.

Also wieso verwendet man Exceptions für eine Stufe?
Als Entwickler der Libraryfunktion fopen kannst du ja nicht wissen, wie der Aufrufer verfahren möchte. Möchte er den Fehler selbst behandeln oder sich nicht darum kümmern?
Na klar kannst du auch "if (f == NULL) return NULL" als Aufrufer schreiben, diesen Boilerplate nehmen dir Exceptions ab.

Aber damit hat man den Code ja wieder zu einem If-Else gemacht, das einfach stattdessen try-catch heisst:
Mit finally-Konstrukten geht das ein bisschen besser, aber immer noch nicht zufriedenstellend:
Java:
void f() {
  try {
    g();
    h();
  }
  finally {
    if (gHasBeenExecuted) {
      // revert g
    }
  }
}

Eine echte Lösung wäre etwas wie Transactions:
Ui, hat das eine general-purpose (nein, nicht SQL :p) Programmiersprache?
 
Jo. Gerade heute wieder einen Kommentar auf eine Antwort gelesen:
Das ist noch gar nichts ... wie wäre es mit offenem Machtmissbrauch ganzer Gruppen von Moderatoren, mit OK von ganz oben? usw.usw.
Nur kommt das in den Vorlesungen nicht so rüber...
Naja, solhe Leute gibts eben überall - auch als Professoren auf der Uni.

Hui, wird da Pi in die tausendste Stelle berechnet oder 1GB Daten durchsucht?
Es liefert die aktuelle Zeit am Server als Timestamp. Das ist alles :rolleyes:
...Inzwischen hab ich meine "Lobbyarbeit" so weit gebracht, dass auch der Vorgesetzte versteht, dass man so nicht arbeiten kann

Warum wird nicht erzwungen, dass Exceptions nur eine Stufe durchdringen können, um es überschaubar zu halten?
Naja, was ist dann überhaupt noch der Sinn von Exceptions?

Doch, so oder so ähnlich machen das recht viele Leute.
Ja leider - wollte ausdrücken, dass das fast nie sinnvoll ist.

Bis heute gibt es keine klare Lösung für dieses Problem.
...was Beweis ist, dass Exceptions eben nicht perfekt sind bzw. manche Sachen umständlicher als vorher machen.
(Aber wir hier erwarten uns ja auch nichts Perfektes - manche Leute aber scheinbar schon, udn denken dann auch es ist schon perfekt)

...
Zu Transactions:
In Sprachen mit Möglichkeiten für Nebenwirkungen (= alles außerhalb vom Programm/Thread/...) wird das wohl kaum möglich sein...
 
Zuletzt bearbeitet:
Schön, das der Thread weitergeführt wird :) Diesen Meinungsaustausch finde ich immer sehr bereichernd.

Kannst du. Und was machst du, wenn er NULL ist? Fehlerausgabe auf stderr und exit(0)? Eine Exception werfen?
Kommt sehr drauf an. Wenn der Wert erwartet NULL sein kann (nach malloc/fopen/etc.), dann gebe ich i.d.R. ein return mit einer negativen Zahl aus und lasse den Caller entscheiden.
Bei Parametern mache ich oft ein assert an den Anfang der Funktion, ganz im Sinne von Design By Contract.
Assertions sind eine sinnvolle Ergänzung zu Exceptions
Assertions sind m.E. anders als Exceptions: Sie sollten nicht für erwartetes oder seltenes verwendet werden, sondern als Indikatoren für Fehler während der Entwicklung/des Testens. Im Releasemode werden sich auch meist deaktiviert.
Fehlerhafte Schnittstellenbenutzung kann sehr wohl auftreten und der Anwender muss darüber informiert werden und muss die Exception ggf. abfangen können.
Da bin ich anderer Meinung. Bibliotheken sollten keine Exceptions verwenden, da sie den Anwender zwingen, auch Exceptions zu verwenden. In Java macht das nichts, in anderen Sprachen, wie C++, schon. Assertions in Debug-Bibliotheken: Ja, klar, gerne! Unerfüllte Preconditions mit asserts zu fangen erspart einem sehr viele mühsame Fehler. Im Releasemode hingegen sollten diese Checks aus Performancegründen deaktiverbar sein, was Exceptions halt prinzipbedingt nicht sind.
Wenn die Parameter, die man an eine Schnittstelle übergibt, den Preconditions nicht entsprechen könnten, muss der Caller dafür sorgen, dass die Preconditions erfüllt sind, d.h. der Caller soll die Checks machen, nicht der Callee. Denn sonst würden die "guten" Programmierer, die nur die "guten" Parameter übergeben, immer gebremst. Bei Funktionen wie fopen, die ohnehin schnarchlangsam sind, machen mehr Checks nicht viel aus, aber sobald man in einen Loop geht, zählt jede Instruktion.

Zugegeben: eine AssertionException kann man auch abfangen und je nach Programmiersprache kann man auch den zu werfenden Ausdruck angeben, sodass die Linie zwischen Assertion und Exception verschwimmt.
Ja, aber das finde ich dann echt skurril. Eine Assertion sollte dann verwendet werden, wenn man nicht vorhat, sich wieder zu fangen (denn sonst könnte man ja "normal" mit ifs prüfen).

Die richtige Lösung wäre es, immer ein Optional<T> zurückzugeben.
Ja, so mache ich das auch immer (oft etwas mühsamer mit success als Rückgabewert und dem Wert als Parameterreferenz, aber sinngemäss dasselbe).

Als Entwickler der Libraryfunktion fopen kannst du ja nicht wissen, wie der Aufrufer verfahren möchte. Möchte er den Fehler selbst behandeln oder sich nicht darum kümmern?
Wenn ich der Entwickler wäre, würde mich das ehrlich gesagt nicht interessieren. Der Aufrufer hat sich an meine Spezifikation zu halten, Punkt. Ich schreibe meine Funktion so, dass sie das Maximum an Effizienz erreicht. Und wie gesagt verwende ich keine Exceptions, wenn ich Libraries schreibe, da ich den Nutzer nicht zur Benutzung von Exceptions zwingen will.
Na klar kannst du auch "if (f == NULL) return NULL" als Aufrufer schreiben, diesen Boilerplate nehmen dir Exceptions ab.
Hier sind wir am Kern der Sache: Ja, Exceptions nehmen den Boilerplate ab. Aber sie tun das ungefragt.
Java:
myfunc();
System.out.println("Hello World");
Wird "Hello World" ausgegeben? Nein, myfunc() hat in einer Kette von Funktionaufrufen einen Funktionsaufruf, der 42 Funktionen tief geht und mit einer Wahrscheinlichkeit von 10e-6 eine Exception wirft, die nicht gefangen wird. Man hat keine Chance, das zu sehen; man muss einen Debugger verwenden, was aber immer noch schwierig ist, da die Exception nicht deterministisch auftritt. Ok, man kann sagen, dass das in die Spezifikation gehört, und man hätte damit sicher recht. Aber gute Spezifikationen sind ja bekanntlich rar.
Im Gegensatz dazu:
C:
int ok = myfunc();
printf("Hello World\n");
0) Ich sehe immer, dass es einen Rückgabewert hat. Mit C++17 gibt es mit [[nodiscard]] sogar die explizite Anweisung, den Rückgabewert nicht zu ignorieren. (Warnung)
1) Ich kann aussuchen, ob mich das Resultat von myfunc() zu diesem Zeitpunkt überhaupt interessiert.
2) Ich kann den Fehler ignorieren
3) Ich kann den Fehler behandeln

Das Argument des Boilerplate ist aber tatsächlich ernstzunehmen. Es ist nicht zu leugnen, dass Exceptions zum Zeitpunkt des Schreiben des Codes sehr praktisch sind, da Programmierer bei Fehlern gerne und oft sagen: "Ist nicht mein Problem". In C/C++ hat man dann gröbere Probleme, das Problem wieder zu lokalisieren, mit Exceptions sieht man den Ursprung aber recht schnell (ich mache es jeweils mit assert(false && "Message"), was bisher immer recht gut funktionierte).
Weniger Boilerplate ist mir auch ein Anliegen, aber es muss doch einen anderen Weg geben.

Mit finally-Konstrukten geht das ein bisschen besser, aber immer noch nicht zufriedenstellend:
Ja, finally wurde ja exakt eingeführt, um das Problem, das Exceptions konstruieren, wieder aufzuräumen. Aber gerade in diesem Beispiel: Wenn man nicht nur 2, sondern 100 Funktionen nacheinander ausführen würde, braucht man ja auch im finally wieder 100 checks; man gewinnt also nichts.
Ui, hat das eine general-purpose (nein, nicht SQL :p) Programmiersprache?
Dieses Beispiel habe ich mir gerade aus den Fingern gesaugt, ich glaube nicht, dass es eine Sprache gibt, die genauso aussieht. Aber:
http://en.cppreference.com/w/cpp/language/transactional_memory
(Experimental)
Aber ich meine auch gar nicht "echte" transactions, sondern etwas in diese Richtung. Dazu auch:
Zu Transactions:
In Sprachen mit Möglichkeiten für Nebenwirkungen (= alles außerhalb vom Programm/Thread/...) wird das wohl kaum möglich sein...
Als Beispiel:
Code:
int i;

void f() {
    print("Setting i");
    i = 42;
} : onabort() {
    print("Aborted");
    i = 0;
}
Hier ist klar: Wenn i concurrent accesses hat, ist das nicht genügend. Ebenfalls kann ein print() nicht rückgängig gemacht werden. Aber macht das hier etwas? Ich brauche ja nicht "pure" transactions, nur ein leichteres RAII. Oder, vielleicht ein etwas sinnvollerer Anwendungszweck:
Code:
void* ptr;
void* ptr2;
void f() {
    ptr = malloc(123);
    ptr2 = malloc(123);
} : onabort() {
    free(ptr);
    free(ptr2);
}
Das zweite Beispiel wird zwar schon mit RAII gelöst, aber dort zahlt man ja immer mit teuren Kopien/Destruktoraufrufen wo immer ein Wert verwendet wird. C++ führte ja u.a. für die Vermeidung von Aufrufen von Konstruktor/Destruktor von grösseren Objekten References ein, die aber auch wieder die Probleme von Pointern haben (indirection). RAII ist sehr breitband: Fängt fast alles, ist aber auch sehr teuer, gerade, wenn man noch virtual Destructors hat oder inlining aus einem anderen Grund nicht möglich ist.

Naja, was ist dann überhaupt noch der Sinn von Exceptions?
Sag du's mir ;)
(Bisschen weniger Boilerplate lasse ich, wie schon gesagt, gelten)

...was Beweis ist, dass Exceptions eben nicht perfekt sind bzw. Sachen umständlicher als vorher machen.
Ein bisschen wie letzte Frage, aber: Warum hat sie denn jede Sprache, die einigermassen cool sein will? Gewohnheit?

Gruss
cwriter
 
Zuletzt bearbeitet:
Bei Parametern mache ich oft ein assert an den Anfang der Funktion, ganz im Sinne von Design By Contract.
Aber wenn die Asserts im Releasemode abgeschaltet werden, läuft du sehr stark Gefahr, enorm inkonsistente Zustände zu erreichen. Der Endnutzer bekommt nicht mal eine Fehlermeldung, wie es bei Exceptions der Fall wäre - auch wenn sie bis zur main hochblubbern. Außerdem kann dein Programm für nichts mehr garantieren, wenn es mit externen Schnittstellen arbeitet, etwa: Wird der Motor zu schnell angesteuert? Hält es sich an HTTP Requests Limits meines eingekauften API Endpoints von {Google, Microsoft, ...}? Werden Konfigurationsdateien vielleicht auf der Festplatte falsch persistiert?

Deswegen finde ich {Exceptions, return -1, return NULL} unbedingt notwendig bei Schnittstellen von Bibliotheken. Innerhalb einer Bibliothek kann man sich meinetwegen auf Asserts verlassen. Da kommt es allerdings auch wieder auf die Größe der Bibliothek an. Wann fangen die Interna einer Bilbiothek an, selbst Schnittstellen zu besitzen?

Assertions sind m.E. anders als Exceptions: Sie sollten nicht für erwartetes oder seltenes verwendet werden, sondern als Indikatoren für Fehler während der Entwicklung/des Testens. Im Releasemode werden sich auch meist deaktiviert.
Genau! Fehlbenutzung einer Bilbiotheksschnittstelle gehört für mich zu etwas Erwartetem.

Im Releasemode hingegen sollten diese Checks aus Performancegründen deaktiverbar sein, was Exceptions halt prinzipbedingt nicht sind.
Von mir aus sind dann auch Errorcodes via return/Parameter in Ordnung, siehe oben. Hauptsache, es wird irgendwie signalisiert und nicht stillschweigend übersprungen, wie im Falle deaktivierter Assertions im Releasemode.

Der Aufrufer hat sich an meine Spezifikation zu halten, Punkt.
Das kann man leider nicht erwarten. Man könnte vielleicht hoffen, dass dies der Compiler und Typchecker übernehmen. Das geht auch zum Teil, z. B. für Werte != null und Typconstraints für Generics. Allerdings können Spezifikationen arbiträr kompliziert werden. Interessant wird's, wenn du deine Funktionen so auslegst, dass sie einen zusätzlichen Parameter erwarten, nämlich einen Beweis dafür, dass die Spezifikation eingehalten worden ist. Ein solches System dafür kenn ich: MMT. Auch wenn das theoretisch imo sehr cool klingt, so ist der Sprung zu Spezifikationen aus der realen Welt ein seeehr großer!

Aber ich meine auch gar nicht "echte" transactions, sondern etwas in diese Richtung.
Genau, so habe ich das auch von dir oben verstanden ;) Transaktionen mit onabort-Methoden im Sinne einer besten Approximation an Ungeschehenmachen und Konsistenzwiederherstellung.

(Bisschen weniger Boilerplate lasse ich, wie schon gesagt, gelten)
Übrigens kapseln Exceptions auch Fehlerinformationen wie Funktion, Dateiname, Zeilennummer, Fehlernachricht und auch arbiträte Daten desjenigen, der sie geworfen hat. Wie machst du das ohne Exceptions?
Einfach auf stderr zu schreiben kommt nicht an dasselbe ran m. E.
 
Aber wenn die Asserts im Releasemode abgeschaltet werden, läuft du sehr stark Gefahr, enorm inkonsistente Zustände zu erreichen.
Aber Asserts waren ja immer da, um die Fehler des Programmierers am Ursprung abzufangen, nicht, um die Fehler einer Umgebung zu fangen. Oder anders gesagt: An Orten, wo Nichtdeterminismus erwartet wird, braucht es etwas anderes als Asserts. Nutzereingaben z.B. Aber wenn ich mich nicht darauf verlassen kann, dass der Programmierer seine Arbeit gemacht hat und ungetesteten Code auf die Nutzer loslässt (release), dann könnte man gar keine Programme mehr haben. Man müsste ja bei jedem Funktionsaufruf, nach jedem Statement, nach jeder Expression nach allen möglichen Side-Effects testen. Kann ja sein, dass der Programmierer mir in den Speicher gespuckt hat, oder eine Klasse speichert und nach einem Programmneustart wieder in den Speicher lädt und erwartet, dass alle Pointer noch identisch sind, etc...
Der Endnutzer bekommt nicht mal eine Fehlermeldung, wie es bei Exceptions der Fall wäre - auch wenn sie bis zur main hochblubbern.
Vor 5-10 Jahren war ja die Welle der 1000 Java-Programme. Ich hatte noch nie zuvor so viele Fehlermeldungen auf dem Bildschirm gesehen. Allermeistens NullPointerException oder OutOfBoundsException. Nützt mir das als Endanwender etwas? Ein Programmierer, der denkt, dem Nutzer eine Exception vorsetzen zu können, kann das Programm auch einfach abstürzen lassen. Ein Endnutzer sagt nicht "Ach geil, eine hübsche generische Fehlermeldung. Wenn's einfach ein "Programm reagiert nicht mehr" wäre, wäre ich angepisst, aber eine Exception gefällt mir".

Außerdem kann dein Programm für nichts mehr garantieren, wenn es mit externen Schnittstellen arbeitet, etwa: Wird der Motor zu schnell angesteuert?
Wie gesagt: Out of control. Bei wirklich wichtigen Anwendungen nützen asserts dann ja meist auch nichts, weil dort bewiesen werden muss, dass die Asserts nicht fehlschlagen können. Bei nicht-kritischen Anwendungen kann auch ein Absturz in Kauf genommen werden.
Hält es sich an HTTP Requests Limits meines eingekauften API Endpoints von {Google, Microsoft, ...}? Werden Konfigurationsdateien vielleicht auf der Festplatte falsch persistiert?
Ok, aber wenn man die Anforderung "der, dem ich einen Auftrag gebe, muss prüfen, ob ich nicht dumm bin" weiterzieht, müsste jede Stufe bis hin zum Netzwerkstack überprüfen, ob du das darfst. Irgendwo muss die Grenze gezogen werden, ab wo es halt schiefläuft. Aber die Erfahrung zeigt: Wenn man überprüft, dass man das richtige übergibt, und rekursiv annimmt, dass alle Aufgerufenen das auch tun, hat man die Anforderung erfüllt.
Um wieder auf Asserts zurückzukommen: Wenn man etwas prüfen will, von dem man annimmt, dass es in Produktivumgebungen vorkommt, darf man es nicht mit asserts prüfen, sondern macht ifs (oder halt Exceptions, wenn man die bevorzugt).
Man ist immer selbst verantwortlich für das, was man in Auftrag gibt. Oder gehst du nach einem Fehlkauf zum Verkäufer und sagst, er sei schuld, weil er dir das, was du wolltest, verkaufte? Und vor allem: Beim ersten Mal, wenn du etwas kaufst und noch nicht ganz sicher bist, ob das richtig ist, hast du sicher gerne eine Beratung (assert). Aber wenn du dann täglich vorbeikommst und dasselbe kaufst, willst du dir dann immer ein paar Minuten lang erzählen lassen, was du schon weisst?


Deswegen finde ich {Exceptions, return -1, return NULL} unbedingt notwendig bei Schnittstellen von Bibliotheken. Innerhalb einer Bibliothek kann man sich meinetwegen auf Asserts verlassen. Da kommt es allerdings auch wieder auf die Größe der Bibliothek an. Wann fangen die Interna einer Bilbiothek an, selbst Schnittstellen zu besitzen?
Genau das meinte ich vorhin. Der Caller hat zu prüfen, sonst Checkst du dieselben facts mehrmals, ohne eine Chance, sie zu entfernen, da sie ja innerhalb der Bibliothek sind.
Man könnte sonst ja noch weiter gehen und z.B. Checken, ob die Calling Convention immer korrekt verwendet wird etc. etc.

Genau! Fehlbenutzung einer Bilbiotheksschnittstelle gehört für mich zu etwas Erwartetem.
Ja, beim Programmieren/Debuggen. Wer im Produktivcode eine Schnittstelle falsch verwendet, gehört geschlagen. Bei WebAPIs ist das ein bisschen anders, da sich da ja viel ändert, aber bei nativen Schnittstellen ist das nicht verhandelbar.

Hauptsache, es wird irgendwie signalisiert und nicht stillschweigend übersprungen, wie im Falle deaktivierter Assertions im Releasemode.
Wie gesagt: Asserts beschweren sich, wenn ein Vertrag (Gib mir korrekte Eingaben, ich gebe dir korrekte Ausgaben) gebrochen wird. Nach der Kennenlernphase ("Debuggen") nimmt man an, dass man den Vertrag nun verstanden hat und nicht mehr bricht. Asserts im Releasemode noch zu checken ist viel zu teuer.

Das kann man leider nicht erwarten.
Na dann ist's aber auch nicht mein Problem. Interessiert mich doch nicht, wenn ein Caller falsche Werte eingibt. Z.B. negative Werte bei malloc(). Dann schreibe ich die Eingabe halt zu unsigned um und versuche es damit. Geht nicht? Tja, bekommst halt einen Fehler zurück.
fread() gibt zurück, wie viele Bytes tatsächlich gelesen wurden. Der Anwender ignoriert's und schaut sich Bytes dahinter an? Was kann ich schon dagegen tun?
Übrigens halten es die allermeisten Bibliotheken so: Im Debugmode gibt es einen Assert, im Releasemode schmiert das Programm halt ab.
Wieder als Analogie: Der Autohersteller ist nicht schuld, wenn du das Auto in einen Baum steuerst.

Aber klar: Statische Analyzer können sehr helfen - aber auch nur beim Caller.
Übrigens kapseln Exceptions auch Fehlerinformationen wie Funktion, Dateiname, Zeilennummer, Fehlernachricht und auch arbiträte Daten desjenigen, der sie geworfen hat. Wie machst du das ohne Exceptions?
Einfach auf stderr zu schreiben kommt nicht an dasselbe ran m. E.
Und wie kommt man auf diese Information?
Im Prinzip ist das ja auch Boilerplate: Kein Mensch, der einigermassen bei Sinnen ist, würde Rückgabewerten Meta-Informationen mitgeben. Exceptions machen es, wie du sagst, versteckt. Also dürfte man Exceptions dann gar nicht mehr verwenden, wenn man auch nur den Hauch einer Chance haben will, performant zu sein.

Ich sehe schon, dass Exceptions "angenehm" sind, aber aber ein Rolls Royce ist auch angenehm, wenn du ihn benutzen kannst. Wenn du ihn auch bezahlen musst, sieht es wieder anders aus.

Gruss
cwriter
 
An Orten, wo Nichtdeterminismus erwartet wird, braucht es etwas anderes als Asserts. Nutzereingaben z.B.
Ah, hier sieht man wohl sehr gut, wo unsere Meinungen auseinander gehen ;) Ich finde, dass Eingaben an einer Bibliotheksschnittstelle (in gewissen Teilen) auch dem Nichtdeterminismus unterworfen sind.

Nützt mir das als Endanwender etwas? Ein Programmierer, der denkt, dem Nutzer eine Exception vorsetzen zu können, kann das Programm auch einfach abstürzen lassen.
Meine Kernbehauptung war, dass du a) den Fehler überhaupt bemerkst und ggf. das Programm terminieren kannst, ganz im Sinne von Defensive Prorgamming und "lieber terminieren als inkonsistent weiterzurechnen". Das ist bei deaktivierten Assertions im Releasemode unmöglich. Ferner kannst du b) den Fehler auch protokollieren. Wie genau man das dem Nutzer präsentiert, ist eher eine UX-Frage. Jedenfalls lässt sich der Fehler im Protokoll wiederfinden (falls der Nutzer Hilfe in Foren oder bei einer Hotline sucht) und ggf. per Telemetrie an dich als Vertreiber schicken lassen.

Um wieder auf Asserts zurückzukommen: Wenn man etwas prüfen will, von dem man annimmt, dass es in Produktivumgebungen vorkommt, darf man es nicht mit asserts prüfen, sondern macht ifs (oder halt Exceptions, wenn man die bevorzugt).
Da stimme ich dir zu!

Aber wenn du dann täglich vorbeikommst und dasselbe kaufst, willst du dir dann immer ein paar Minuten lang erzählen lassen, was du schon weisst?
Da kommt's drauf an, wie teuer das Produkt ist, also wie schlimm es ist, uninformiert unter falschen Annahmen über das Produkt (= inkonsistent) das Produkt zu erwerben (= weiterzurechnen). (Wow "das Produkt das Produkt", ist der Satz grammatikalisch? :D)
Aber selbst bei teuren Produkten wird der Käufer wohl irgendwann relativ selbstsicher werden, weil der Kontext (= Annahmen, Vertrauen in den Verkäufer, Anwendungsfälle des Produkts, ...) immer derselbe ist. Nur als Bibliothek ist das nicht so. Deine Schnittstelle wird von jedermann genutzt, ggf. auf verschiedenen Geräten, OS, Sprachen.

Z.B. negative Werte bei malloc(). Dann schreibe ich die Eingabe halt zu unsigned um und versuche es damit. Geht nicht? Tja, bekommst halt einen Fehler zurück.
Das sollte idealerweise vom Typchecker / Static Analyzer bereits bei der Expression des Funktionsaufrufs im AST abgefangen werden.

Und wie kommt man auf diese Information?
Dateiname, Zeilennummer, Funktionsname, Stack Trace werden alle autogeneriert, aber auch nur wenn die Exception auftritt. Allgemein ist der Performanceverlust in normalen Ablaufszenarien ohne Exceptions Folgender:
  • der von den IF-Bedingungen um den Exceptionwurf herum
  • der des zusätzlich autogenerierten Codes im Assembler
Ich meine mal gelesen zu haben, dass der zweite Punkt gar nicht mal so viel Einfluss auf die Performance zur Laufzeit (d. h. ohne Laden der Executable) hat. Diese Antwort stützt die These. (Es gibt allerdings auch einige 'falsche' Microbenchmarks, z. B. diesen hier.)

Im Prinzip ist das ja auch Boilerplate: Kein Mensch, der einigermassen bei Sinnen ist, würde Rückgabewerten Meta-Informationen mitgeben.

Ist die errno im Falle, dass fopen NULL zurückgibt, nicht auch ein Metawert?


PS: Bevor ich das nächste Mal antworte, lasse ich lieber mal wieder neuen Input von @sheel (oder jeden, der sich angesprochen fühlt) vorher in diesen Thread einfließen ;)
 
Zurück