C++20 Tutorial/Howto: Concepts
-
@VLSI_Akiko sagte in C++20 Tutorial/Howto: Concepts:
Oh keine Sorge, denen zeige ich das nicht.
Aber du zeigst es hier in Tutorials. Hm
Aber ja, wäre vielleicht vernünftiger es wegzulassen um die Leute nicht erst auf Ideen zu bringen.
Genau darum geht es mir. Wobei ich auch selbst kein
noexcept
schreibe, ausser in Fällen wo ich der Meinung bin dass es eben wirklich Sinn macht. Also unsereSpinLock
Klasse hat z.B.noexcept
Lock/Unlock etc. Funktionen. Manche andere ultra-low-level Utility Klassen/Funktionen auch. Aber sonst lasse ich das wie gesagt weg. Undnoexcept(false)
lasse ich wie gesagt sowieso immer weg.Und in Tutorials für Anfänger würde ich es auf jeden Fall weglassen. Ich hab' auch oft genug gesehen dass Anfänger ihre Include-Guards
__FOO_H
nennen, oder ihre Membervariablen_Foo
. Wenn man sie dann fragt warum, dann ist die Antwort oft: ich hab das so in der Standard-Library gesehen, und daher dachte ich mir "das gehört so". Worauf ich damit hinaus will ist, dass Anfänger Dinge die sie in fremdem Code sehen oft einfach nachmachen, ohne verstanden zu haben warum das dort so ist wie es ist. Und dadurch dann oft Fehler machen. Und das ist etwas was beinoexcept
vermutlich nicht zu viel Gutem führt.Wäre es eventuell besser, wenn ich auch eine etwas "professionelle" Sprache bei den Beispielen verwende? Habe ja vor noch einige zu machen.
Pfuh, keine Ahnung. Ein bisschen vielleicht, gibt ja immer Leute die von einem all zu lockeren Ton auf nicht-professionell schliessen. Wobei zu sehr übertreiben muss man es denke ich auch nicht, Programmierer sind da denke ich tendenziell nicht so schlimm wie andere was das angeht.
Was ich empfehlen würde wäre möglichst wenig auszuschweifen. Versuch Beispiele zu finden wo man nicht erst lange erklären muss wozu das überhaupt gut ist. Und beim "Reflection" Beispiel denke ich wäre gut zu zeigen wie man damit Dispatching zwischen verschiedenen Implementierungen machen kann.
Statt
std::tuple
würde sich da mMn. auch eherstd::array
oderstd::span
anbieten. EinprintAll
für ein Tuple ist ja nicht wirklich ein "unrolled loop", es bleibt einem ja gar nichts anderes übrig als es "unrolled" zu machen - die Elemente können ja alle unterschiedliche Typen haben. Beistd::array
oder fixed-sizestd::span
könnte man dagegen schön eine "unrolled" Implementierung + eine mit Loop machen, und dann mit Concepts die Auswahl zwischen den beiden machen (ContiguousSequence
vs.SmallContiguousSequence
oder so).
-
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
Und
noexcept(false)
lasse ich wie gesagt sowieso immer weg.Habe schon bei RAII utility Klassen dem Destruktor noexcept(false) hinzugefügt, da ich das nicht garantieren konnte und kein std::terminate riskieren will.
-
@5cript Ja. Ich hab mir, wie ich das geschrieben habe, überlegt ob ich auf den Fall Destruktoren eingehen sollte. Hab mich dann dagegen entschieden weil ich mir dachte der Fall ist schon sehr speziell. Aber ja, das ist ein Fall wo man
noexcept(false)
braucht, und wo ich es auch schreiben würde. Also vorausgesetzt ich will dass die Exception fliegt. Bisher hat mir immer eincatch (...)
ausgereicht.Also korrigierte Version: ich lasse
noexcept
weg, wenn der Default das ist was ich haben möchte.
-
Danke fürs z'sammschreiben!
@VLSI_Akiko sagte in C++20 Tutorial/Howto: Concepts:
Concept als Tybbeschränkung
-
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
@5cript Ja. Ich hab mir, wie ich das geschrieben habe, überlegt ob ich auf den Fall Destruktoren eingehen sollte. Hab mich dann dagegen entschieden weil ich mir dachte der Fall ist schon sehr speziell. Aber ja, das ist ein Fall wo man
noexcept(false)
braucht, und wo ich es auch schreiben würde. Also vorausgesetzt ich will dass die Exception fliegt. Bisher hat mir immer eincatch (...)
ausgereicht.Also korrigierte Version: ich lasse
noexcept
weg, wenn der Default das ist was ich haben möchte.Also ob man es dranschreibt oder nicht, ist egal. Ehrlich gesagt hatte ich einen kleinen Test eingebaut hier in diesem Satz:
@VLSI_Akiko sagte in C++20 Tutorial/Howto: Concepts:Auch die Annahme, dass noexcept wegzulassen oder noexcept(false) zu schreiben, sei das Gleiche, trifft auch nicht zu.
Wundert mich ein bisschen, dass du mich deswegen nicht zerrupft hast. Also
noexcept(false)
ist das Äquivalent zum weglassen. Es gibt da keinen Unterschied. Ich schreibe es aber, weil ich damit ganz klar sagen will: Ich habe mir Gedanken darüber gemacht und garantiere, dass hier Exceptions fliegen, wenn was nicht passt. Ansonsten schreibe ich dienoexcept
schon ganz automatisch, wenn es möglich ist. Und da hat hustbaer recht, wenn man Anfänger oder auch Fortgeschrittener ist, sollte man sich da lieber keine Gedanken drüber machen, außer man ist bereit eine Reference nach der anderen zu welzen und sich ganz tief einzulesen. Vielleicht sollte ich an bei den kleinen Funktion erklären warum ich da einnoexcept
dranschreibe. Oder ich lasse es komplett weg, was sich aber komisch anfühlt, weil es gegen meine gängige Praxis geht.@Swordfish sagte in C++20 Tutorial/Howto: Concepts:
Danke fürs z'sammschreiben!
@VLSI_Akiko sagte in C++20 Tutorial/Howto: Concepts:
Concept als Tybbeschränkung
Ich sollte echt aufhören die Sachen hier mal schnell neben der Arbeit zu tippen (während ich auf den Compiler warte.) Danke, wird korrigiert.
-
Wundert mich ein bisschen, dass du mich deswegen nicht zerrupft hast.
Ich hatte darauf geantwortet, hast du das überlesen?:
Auch die Annahme, dass noexcept wegzulassen oder noexcept(false) zu schreiben, sei das Gleiche, trifft auch nicht zu.
Also mir fallen jetzt nur zwei Fälle ein wo es einen Unterschied macht:
- Destruktoren
- inline = default von "special member functions"
Davon abgesehen: wo hab ich denn geschrieben dass ich das annehme?
Weil ich es nicht mag wenn mir jemand unterstellt ich würde irgendwas annehmen was
a) nicht stimmt und
b) ich gar nicht annehme und
c) wo ich auch nirgends etwas geschrieben habe aus dem man schliessen könnte ich würde es annehmen.
Solche Unterstellungen nerven schon ein bisschen.Ansonsten schreibe ich die noexcept schon ganz automatisch, wenn es möglich ist.
Was machst du bei Refactorings/Änderungen? Also angenommen du hast Funktion X, und die kann
noexcept
sein und deswegen schreibst du es dran. Funktion X wird nun in Funktionen Y1...Yn verwendet, und Funktionen Y1...Yn werden wiederrum in Funktionen Z01...Znn verwendet. Durch dasnoexcept
von X können davon auch vielenoexcept
sein und du schreibst es überall dran. Und nun willst du X ändern - und es kann auf einmal nicht mehrnoexcept
sein. Wodurch jetzt dasnoexcept
in vielen Funktionen auf einmal gefährlich wird. Und dann machst du ... was? Weinen.noexcept
einfach überall dranzuschreiben wo es geht, ist mMn. wie gesagt der total falsche Weg. Wenn man vielnoexcept
schreiben will, dann müsste man sich bei jeder Funktion gut überlegen ob es vernünftig ist anzunehmen dass die jeweilige Funktion jetzt und für alle Zeitennoexcept
sein kann. Wozu man oft eine Glaskugel bräuchte, die man halt nicht hat. Und dadurch dass du vom Compiler keinerlei Hilfestellung bekommst die betroffenen Funktionen Y1...Yn/Z01.../Z01...Znn zu finden, wird die Sache nicht besser.Im Prinzip ist es ein ähnliches Problem wie mit
const
. Nur dass es zwei wichtige Unterschiede gibt:- Bei
const
sagt dir der Compiler wenn was nicht passt - Bei
const
gibt es so-gut-wie-immer eine Möglichkeit die Funktion weiterhinconst
zu lassen, trotz Änderungen (z.B. intern synchronisieren)
-
Ist ggf. schon der Denkansatz falsch? Eine Negation verneinen ist schon etwas schwierig. Mir z.B. wäre es lieber, wenn so etwas wie; "kann Exception XY werfen" lieber. Ansonsten müsste ich ja um jeden Funktionsaufruf ein try catch Konstrukt bauen.
Einfach ausgedrückt, jeder Entwickler kennzeichnet seine Methoden/Funktionen, wenn diese eine Exception X werfen. Dann kann ich diese ggf. in einer aufrufenden Methode fangen und verarbeiten oder weitere geben. Wenn ich weitergebe, kennzeichne ich meine Methode/ Funktion damit.
Oder sehe ich das zu einfach?
-
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
Wundert mich ein bisschen, dass du mich deswegen nicht zerrupft hast.
Ich hatte darauf geantwortet, hast du das überlesen?:
Oh stimmt, ich verliere langsam den Überblick. Ich editiere ja zur gleichen Zeit des ersten Post, damit er besser wird. Durch dieses hin und her entgeht mir wohl auch das eine oder andere. Hmm, und dass ich mir D2R zugelegt habe, ist auch nicht gerade hilfreich.
Ansonsten schreibe ich die noexcept schon ganz automatisch, wenn es möglich ist.
Was machst du bei Refactorings/Änderungen? Also angenommen du hast Funktion X, und die kann
noexcept
sein und deswegen schreibst du es dran. Funktion X wird nun in Funktionen Y1...Yn verwendet, und Funktionen Y1...Yn werden wiederrum in Funktionen Z01...Znn verwendet. Durch dasnoexcept
von X können davon auch vielenoexcept
sein und du schreibst es überall dran. Und nun willst du X ändern - und es kann auf einmal nicht mehrnoexcept
sein. Wodurch jetzt dasnoexcept
in vielen Funktionen auf einmal gefährlich wird. Und dann machst du ... was? Weinen.Oh nein, bei Refactoring mache ich das nicht, bin doch kein Masochist. Ich mache das nur bei Sachen die ich komplett selber schreibe und wo auch kein anderer dazwischenfunkt. Ein schönes Beispiel dafür sind 2/3/4D Vector Implementierungen bzw Hilfsfunktionen die ich öfters brauche, wie zum Beispiel diese Templates.
template <Concept::Number T> constexpr inline std::tuple<T,T> minMax(const T val1, const T val2) noexcept { return val1 > val2 ? std::tuple<T,T>(val2, val1) : std::tuple<T,T>(val1, val2); } template <Concept::Number T> constexpr inline std::tuple<T,T> maxMin(const T val1, const T val2) noexcept { return val1 < val2 ? std::tuple<T,T>(val2, val1) : std::tuple<T,T>(val1, val2); }
noexcept
einfach überall dranzuschreiben wo es geht, ist mMn. wie gesagt der total falsche Weg. Wenn man vielnoexcept
schreiben will, dann müsste man sich bei jeder Funktion gut überlegen ob es vernünftig ist anzunehmen dass die jeweilige Funktion jetzt und für alle Zeitennoexcept
sein kann. Wozu man oft eine Glaskugel bräuchte, die man halt nicht hat. Und dadurch dass du vom Compiler keinerlei Hilfestellung bekommst die betroffenen Funktionen Y1...Yn/Z01.../Z01...Znn zu finden, wird die Sache nicht besser.Es ist im Prinzip alles write-once Code wie oben bei den Templates. Wenn ich mein Tutorial zu Coroutinen mache, wirst du sogar sehen, dass es erforderlich ist. Die Coroutinen verlangen nämlich einen spezifische promise_type Implementierung, bei dessen Funktionen ein
noexcept
Bestandteil der Funktionssignatur ist (zumindest in der Implementierung vom gcc 11). Ich habe auch den ersten Tutorial-Post dahingehend angepasst. Ich erkläre, was es damit auf sich hat und was man machen sollte, wenn man nicht weiß, was dahintersteckt.Im Prinzip ist es ein ähnliches Problem wie mit
const
. Nur dass es zwei wichtige Unterschiede gibt:- Bei
const
sagt dir der Compiler wenn was nicht passt - Bei
const
gibt es so-gut-wie-immer eine Möglichkeit die Funktion weiterhinconst
zu lassen, trotz Änderungen (z.B. intern synchronisieren)
Oh, keine Sorge, der Compiler sagt dir auch, wenn das
noexcept(false)
(also fehlendesnoexcept
) nicht gesetzt ist. Ja okay, ist nicht das gleiche, aber offenbar scheint die Diagnostig mehr zu können.~/Repositories/test_code.git/coroutine (git)-[main] % g++ -std=c++23 -pthread -flto -fconcepts-diagnostics-depth=10 -W -Wall -Wextra -s -Os -o test test.cxx test.cxx: In function ‘Generator<long int> genFib()’: test.cxx:37:14: error: the expression ‘Generator<long int>::promise_type::final_suspend’ is required to be non-throwing 37 | auto final_suspend() //noexcept | ^~~~~~~~~~~~~
- Bei
-
@VLSI_Akiko sagte in C++20 Tutorial/Howto: Concepts:
Oh nein, bei Refactoring mache ich das nicht, bin doch kein Masochist.
So war das nicht gemeint. Ich meinte: Du schreibst sowas (neuen Code):
void foo(char const* s) noexcept { ... puts(s); // logging -> best effort -> ignore errors }; // irgendwo anders void bar() noexcept { ... if (blah) foo("meh"); }; // irgendwo nochmal ganz wo anders void baz() noexcept { ... bar(); };
Und später wird
foo
irgendwann geändert:void foo(char const* s) noexcept { ... functionWhichTakesAStdStringArgument(s); // important, must not ignore errors puts(s); // logging -> best effort -> ignore errors };
Jetzt sollte man da mMn. das
noexcept
wegmachen, weil ja derstd::string
ctor verwendet wird:void foo(char const* s) { ... functionWhichTakesAStdStringArgument(s); // important, must not ignore errors puts(s); // logging -> best effort -> ignore errors };
Und genau da hast du dann ein Problem. Weil dir da der Compiler genau gar nicht hilft.
bar
undbaz
sind immer nochnoexcept
, und der Compiler schweigt sich aus. Genaugenommen kann man schon vorher ein Problem bekommen, denn der Compiler schweigt sich auch aus wenn du dasnoexcept
beifoo
dranlässt. Und dass die Änderung auf einmal dazu führt dass eine Exception fliegen könnte, das übersieht man schnell.Und selbst wenn dir der Compiler und/oder die IDE alle Stellen anzeigen könnte wo jetzt ein
noexcept
zu viel ist: du kannst nicht zig oder hunderte Stellen durchgehen wo solche Funktionen verwendet werden, und bei allen checken ob die auch Exception-Safe sind. Die Funktionen waren ja schliesslichnoexcept
, also darf man auch Code schreiben der sich darauf verlässt dass da eben keine Exception rausfliegt.Es ist im Prinzip alles write-once Code wie oben bei den Templates.
Dann haben wir unterschiedliche vorstellung davon was "
noexcept
überall wo es geht" bedeutet. Für mich bedeutet das: eben überallnoexcept
dranschreiben wo die Implementierung aktuell garantiert dass keine Exceptions fliegen. Das sind viele Funktionen, und eben nicht bloss "write once" Code. Und da wirklich überallnoexcept
dranzuschreiben ist mMn. eben wirklich keine gute Idee.Ein schönes Beispiel dafür sind 2/3/4D Vector Implementierungen bzw Hilfsfunktionen die ich öfters brauche, wie zum Beispiel diese Templates.
Wieso nicht gleich
template <class T> constexpr inline std::tuple<T,T> minMax(T a, T b) noexcept( noexcept(a < b) && noexcept(std::tuple<T,T>{std::move(a), std::move(b)}) ) { return a < b ? std::tuple<T,T>{std::move(a), std::move(b)} : std::tuple<T,T>{std::move(b), std::move(a)}; }
?
Oh, keine Sorge, der Compiler sagt dir auch, wenn das
noexcept(false)
(also fehlendesnoexcept
) nicht gesetzt ist.Nicht in dem Fall den ich meine (siehe oben).
-
@Helmut-Jakoby sagte in C++20 Tutorial/Howto: Concepts:
Ist ggf. schon der Denkansatz falsch? Eine Negation verneinen ist schon etwas schwierig. Mir z.B. wäre es lieber, wenn so etwas wie; "kann Exception XY werfen" lieber.
OK. Mir nicht
Ansonsten müsste ich ja um jeden Funktionsaufruf ein try catch Konstrukt bauen.
Nö, du musst nur Exception-sicheren Code schreiben. Die Entscheidung zwischen "basic" und "strong" ist oft nicht trivial. Im Zweifelsfall, speziell bei Klassen deren Objekte typischerweise langlebig sind, halt "strong".
Bzw. es gibt auch noch eine Zwischenstufe von "basic" und "strong". "Basic" garantiert ja nur dass das Objekt noch fehlerfrei zerstört werden kann, aber nicht dass es noch einen 100% konsistenten Zustand hat. Und "strong" garantiert "all or nothing". Die Zwischenstufe wäre dann die Garantie, dass sämtliche Invarianten des Objekts erhalten bleiben - Änderungen am Objekt sind aber erlaubt.
Einfach ausgedrückt, jeder Entwickler kennzeichnet seine Methoden/Funktionen, wenn diese eine Exception X werfen. Dann kann ich diese ggf. in einer aufrufenden Methode fangen und verarbeiten oder weitere geben. Wenn ich weitergebe, kennzeichne ich meine Methode/ Funktion damit.
Oder sehe ich das zu einfach?Das ist halt extremer Aufwand. Java macht das mehr oder weniger so - also wenn man mal die "unchecked exceptions" ignoriert.
In C++ hat sich aber eher die Ansicht durchgesetzt dass es nicht wirklich wichtig ist welche Exceptions eine Funktion werfen könnte, sondern nur ob sie irgendwas werfen kann. Bei bestimmten Dingen ist es halt wichtig zu wissen dass sie keine Exceptions werfen können. Standardbeispiel wäre move-Konstruktion von Elementen bei Containerklassen wie
std::vector
. Wenn diese garantiertnoexcept
ist, dann kann man Reallocation mit viel weniger Overhead implementieren.Der interessante und besondere Fall ist auch nicht "can throw" sondern "cannot throw". Daher ist der Default (mit wenigen Ausnahmen) auch "can throw", und das Keyword hat die Bedeutung "cannot throw". Und ich empfehle auch nicht es in verneinter Form irgendwo dranzuschreiben, wo der Default sowieso "can throw" ist.
-
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
Dann haben wir unterschiedliche vorstellung davon was "noexcept überall wo es geht" bedeutet. Für mich bedeutet das: eben überall noexcept dranschreiben wo die Implementierung aktuell garantiert dass keine Exceptions fliegen. Das sind viele Funktionen, und eben nicht bloss "write once" Code. Und da wirklich überall noexcept dranzuschreiben ist mMn. eben wirklich keine gute Idee.
Okay, eventuell bringt die Aussage "noexcept überall da, wo es Sinn macht (auch langfristig)" es besser auf den Punkt.
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
Wieso nicht gleich
template <class T> constexpr inline std::tuple<T,T> minMax(T a, T b) noexcept( noexcept(a < b) && noexcept(std::tuple<T,T>{std::move(a), std::move(b)}) ) { return a < b ? std::tuple<T,T>{std::move(a), std::move(b)} : std::tuple<T,T>{std::move(b), std::move(a)}; }
Die beiden Templates waren durch das Concept "Number" beschränkt, also auf gebrochene und natürliche Zahlen. In diesen Templates liegen sie immer auf dem Stack bzw den Registern. Die move-Semantic wäre korrekt, aber es würde nie ein Move stattfinden (bzw die Umwandlung zu eine r-value Reference wäre bedeutunglos). Es wäre verschwendete Mühe, dass so ausführlich zu schrieben. Wäre es ein Template, dass Typen akzeptiert, wo Teile des Typen auf dem Heap liegen, wäre das der korrekte Weg.
-
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
In C++ hat sich aber eher die Ansicht durchgesetzt dass es nicht wirklich wichtig ist welche Exceptions eine Funktion werfen könnte, sondern nur ob sie irgendwas werfen kann. Bei bestimmten Dingen ist es halt wichtig zu wissen dass sie keine Exceptions werfen können. Standardbeispiel wäre move-Konstruktion von Elementen bei Containerklassen wie std::vector. Wenn diese garantiert noexcept ist, dann kann man Reallocation mit viel weniger Overhead implementieren.
Vlt. etwas off topic, aber warum soll es nicht wichtig sein, welche exceptions eine Funktion werfen könnte?
Ich frage mich irgendwie relativ oft, ob und welche exceptions fliegen könnten und wie ich diese entsprechend catche. Jedes mal muss ich dann in die Doku gucken, um festzustellen, dass es meisten ja doch nicht drin steht.
Ein aktuelles Beispiel war z.B. Boost Process, wo ich mich fragte, welche der Funktionen eig. ne exception schmeißen könnte -> z.B. Konstruktor mit Pid, pid existiert nicht -> Exception?
Letzendes muss ich halt in die Implementierung gucken, um zu schauen, dass keine exception geschmissen wird.Oder kennt da die restliche C++ Community einen Trick den ich nicht kenne?
-
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
In C++ hat sich aber eher die Ansicht durchgesetzt dass es nicht wirklich wichtig ist welche Exceptions eine Funktion werfen könnte, sondern nur ob sie irgendwas werfen kann. Bei bestimmten Dingen ist es halt wichtig zu wissen dass sie keine Exceptions werfen können. Standardbeispiel wäre move-Konstruktion von Elementen bei Containerklassen wie
std::vector
. Wenn diese garantiertnoexcept
ist, dann kann man Reallocation mit viel weniger Overhead implementieren.Das diese Funktion
noexcept
ist nützt für das Schreiben von Templates rein gar nichts. Dafür gibt es dochAllocator::propagate_on_container_move_assigment
. Wenn dasstd::true_type
ist, darf man optimierten Code verwenden. Das Problem ist, dass sich die Standard Library überhaupt nicht daran hält. Es macht halt bumm, wenn der Allokator dortstd::false_type
definiert hat.Was das Thema Destruktoren betrifft. Da C++ nicht mehrfach Exceptions handeln kann, begibt man sich in Teufelsküche, wenn irgend ein Destruktor werfen darf. Denn wenn es einer darf, dürfen es andere auch, und spätestens beim zweiten Werfen gibt es ein
std::terminate()
. Dann lieber gleich richtig, und beim ersten Fehler es knallen lassen.
-
@VLSI_Akiko sagte in C++20 Tutorial/Howto: Concepts:
Okay, eventuell bringt die Aussage "noexcept überall da, wo es Sinn macht (auch langfristig)" es besser auf den Punkt.
Vermutlich. Und das, also einzuschätzen wo es Sinn macht, ist mMn. auch nix für Anfänger. Bzw. nichtmal unbedingt was für erfahrene Anwendungsentwickler. Sondern was für erfahrene Libraryentwickler.
Die beiden Templates waren durch das Concept "Number" beschränkt, also auf gebrochene und natürliche Zahlen. In diesen Templates liegen sie immer auf dem Stack (...) verschwendete Mühe (...) Wäre es ein Template, dass Typen akzeptiert, wo Teile des Typen auf dem Heap liegen, wäre das der korrekte Weg.
Wie du bemerkt hast hab ich das Concept entfernt Weil ich der Meinung bin dass man das Template nicht unnötig einschränken muss. Das
noexcept
wird dadurch komplizierter. Dafür entfällt die versteckte Abhängigkeit darauf dass das Concept garantiert dass die verwendeten Operationennoexcept
sind.
-
@Leon0402 Welche Exceptions geworfen werden ist mMn. nur dann interessant, wenn es zum Contract gehört. Also wenn z.B. der Contract ist "bei falscher Syntax wird eine
InvalidSyntax
Exception geworfen", dann ist das wichtig, und dann gehört das in die Doku. Wenn die Funktion Speicher anfordert, dann kann & darf sie aber zusätzlich auch nochstd::bad_alloc
werfen. Das muss dann nicht extra erwähnt werden.Der Contract kann aber auch nur sein "im Fall X wird eine (irgendeine) Exception geworfen". Das muss dann nicht unbedingt in die Doku. In C++ ist es üblich Fehler mit Exceptions zu kommunizieren. Wenn in der Doku nix steht und es auch keinen offensichtlichen anderen Weg gibt auf dem Fehler kommuniziert werden, dann ist die Annahme vernünftig dass der Contract ist: "bei Fehlern jeglicher Art wird irgend eine Exception geworfen". (Offensichtlicher anderer Weg wäre z.B. wenn die Funktion nen Error-Code zurückliefert.)
Oder kennt da die restliche C++ Community einen Trick den ich nicht kenne?
Der Trick ist, dass es meistens einfach wurscht ist. Meist ist der Contract "bei Fehlern jeglicher Art wird irgend eine Exception geworfen" auch für eigene Funktionen völlig ausreichend. Und diesen zu implementieren ist super-einfach: man schreibt den Code einfach runter, und achtet bloss* darauf dass man alles "exception safe" implementiert. Also salopp gesagt dass nix schlimmes passiert falls aus irgend einer der aufgerufenen Funktionen eine Exception fliegt. Fangen tut man gar nix, denn falls eine Exception wo rausfliegt, und man die einfach weiter fliegen lässt, hat man ja automatisch den Contract erfüllt.
*: Exception-safe Programmieren ohne haufenweise try-catch ist etwas, womit viele Probleme haben. Nach meiner Erfahrung allerdings hauptsächlich deswegen weil sie meinen es müsse einfacher gehen und/oder weil sie Werkzeuge wie
gsl::finally
bzw. im Notfall haltthrow;
nicht kennen.
Auf dein Beispiel bezogen: wieso musst du wissen welche Exceptions
Boost.Process
schmeisst? Ob kann wie gesagt sehr interessant sein. Aber welche?Und ja, ich weiss dass es manchmal wichtig sein kann welche Art Fehler passiert ist. Also wichtig nicht nur damit man es in ein Logfile schreiben kann, sondern weil man möchte dass das Programm auf bestimmte Fehler anders reagiert. Ich sage ja nur: meistens ist es egal.
Und wenn es nur darum geht eine Fehlermeldung in ein Logfile zu schreiben, dann kann man sowas machen:... } catch (std::exception const& e) { LOG("Error: "s + e.what() + " (type " + typeid(e).name() + ")"); } catch (...) { LOG("Error: unknown exception"); }
-
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
Wenn die Funktion Speicher anfordert, dann kann & darf sie aber zusätzlich auch noch
std::bad_alloc
werfen. Das muss dann nicht extra erwähnt werden.So am Rande frage ich mich, ob man speziell diese Exception nicht nur nicht extra erwähnt, sondern vielleicht sogar komplett ignoriert. Oder kann man ausser in sehr speziellen Ausnahmefällen ein
bad_alloc
irgendwie sinnvoller behandeln, als es einterminate
ohnehin schon tut? Vor allem wenn man wahrscheinlich eh gar keinen Speicher mehr reservieren kann. Damit könnten wahrscheinlich jede Menge Funktionen (vor allem viele Konstruktoren)noexcept
werden. Auch wurde ich abseits von Bugs noch nie wirklich mitbad_alloc
s beworfen, und Bugs sind ja eher ein Fall fürassert
s (mag auf magereren Systemen anders aussehen).
-
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
Vermutlich. Und das, also einzuschätzen wo es Sinn macht, ist mMn. auch nix für Anfänger. Bzw. nichtmal unbedingt was für erfahrene Anwendungsentwickler. Sondern was für erfahrene Libraryentwickler.
Ja, deswegen habe ich ja den Tutorialpost dahingehend angepasst.
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
Wie du bemerkt hast hab ich das Concept entfernt Weil ich der Meinung bin dass man das Template nicht unnötig einschränken muss. Das noexcept wird dadurch komplizierter. Dafür entfällt die versteckte Abhängigkeit darauf dass das Concept garantiert dass die verwendeten Operationen noexcept sind.
Ja schon richtig. Mach ich ja auch so, wenn Templates komplexere Typen akzeptieren, aber im Fall der Zahlen wars nunmal überflüssig.
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
In C++ ist es üblich Fehler mit Exceptions zu kommunizieren.
Ja, und selbst einige C++-Komitee Mitglieder sind sich nichtmal sicher, ob das generell so eine gute Idee war. Denn spätestens wenn man mit std::bad_alloc zu tun hat, also Speicherknappheit, kann soviel Blödsinn passieren, dass es manchmal schwierig ist eine brauchbare Lösung zu finden. Ich bin ganz froh, dass sie aus diesem Grund Möglichkeiten eingebaut haben, hier und da Exceptions abzuschalten oder zu umgehen, wie bei
new (std::nothrow)
.
-
@Finnegan sagte in C++20 Tutorial/Howto: Concepts:
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
Wenn die Funktion Speicher anfordert, dann kann & darf sie aber zusätzlich auch noch
std::bad_alloc
werfen. Das muss dann nicht extra erwähnt werden.So am Rande frage ich mich, ob man speziell diese Exception nicht nur nicht extra erwähnt, sondern vielleicht sogar komplett ignoriert. Oder kann man ausser in sehr speziellen Ausnahmefällen ein
bad_alloc
irgendwie sinnvoller behandeln, als es einterminate
ohnehin schon tut?Das kommt stark auf die Anwendung drauf an. In vielen interaktiven Programmen hängt der Speicherverbrauch stark von den Aktionen des Benutzers ab. Wenn ich z.B. in Photoshop auf "new Layer" klicke wird da u.U. ordentlich viel Speicher angefordert. Wenn da ein
bad_alloc
fliegt, kann man das durchaus vernünftig behandeln. Man muss dem Benutzer deswegen nicht gleich das ganze Programm wegschiessen - was bedeutet dass er alle ungespeicherten Änderungen verliert.In Services, z.B. Web-Services, kann man versuchen eine Fehlermeldung zurückzuschicken. Wenn das wieder schief geht weil kein Speicher verfügbar ist, kann man die Connection einfach trennen. Man muss aber nicht den ganzen Service-Prozess killen.
In Commandline-Programmen fängt man dagegen oft alle Exceptions in
main
und kann dann eine Fehlermeldung z.B. nachstderr
schreiben. Das ist dann wirklich kaum besser als das was die C++ Runtime Library machen würde - die schreibt typischerweise auch ne Meldung nachstderr
und bricht das Programm dann ab.Vor allem wenn man wahrscheinlich eh gar keinen Speicher mehr reservieren kann.
Es kommt stark drauf an wie gross der Block war den man angefordert hat, wo man die Exception fängt und ob der Stack-Unwinding Code Speicher anfordern muss.
Wenn der Block gross war, stehen die Chancen gut dass man noch kleinere Speicheranforderungen machen kann. Und wenn man die Exception weit genug entfernt fängt, stehen die Chancen recht gut dass beim Stack-Unwinding bis dorthin einiges an Speicher freigegeben wird. Wenn der Stack-Unwinding Code natürlich selbst Speicher anfordert, dann ... wird es doofIch kann die fatalistische Einstellung gegenüber
bad_alloc
die viele Entwickler haben (darunter leider auch viele Library-Entwickler) auf jeden Fall nicht ganz verstehen.
-
@VLSI_Akiko sagte in C++20 Tutorial/Howto: Concepts:
Ja schon richtig. Mach ich ja auch so, wenn Templates komplexere Typen akzeptieren, aber im Fall der Zahlen wars nunmal überflüssig.
Naja "minMax" ist ja ein Konzept welches nicht nur für Skalare interessant ist. Vektoren, Matritzen, Tensoren - u.U. mit dynamischer Grösse...
Wobei es dann vermutlich besser wäre wie beistd::min
/std::max
Referenzen zurückzugeben.
-
@hustbaer sagte in C++20 Tutorial/Howto: Concepts:
Naja "minMax" ist ja ein Konzept welches nicht nur für Skalare interessant ist. Vektoren, Matritzen, Tensoren - u.U. mit dynamischer Grösse...
Wobei es dann vermutlich besser wäre wie beistd::min
/std::max
Referenzen zurückzugeben.Ja, es ginge sogar std::string. Amüsanterweise würden dabei auch beide Varianten passieren, da die libstdc++ std::string Implementierung ja auf dem Stack liegen kann, wenn klein genug (24 chars? -> aka small string optimization). Wobei das mit dem Referenzen ja nicht explizit nötig wäre. Dank C++11 r-value Referenzen kann man ja nun "dicke Objekte" zurückgeben (oder non-pointer Geschichten in Container schieben). Woah, kannste dich noch erinnern wie brutal die Performance in pre-C++11 bei so etwas gelitten hat? Ich habe hier noch einen Compiler im Einsatz wo es noch keine STL gab und C++ nicht mal Namespaces kannte. Also C++11 war schon ein echter Meilenstein.