C++20 Tutorial/Howto: Concepts



  • @VLSI_Akiko sagte in C++20 Tutorial/Howto: Concepts:

    Diesmal hast du nicht genau gelesen.

    Würde ich nicht so sagen, nein.

    Es wird alles entrollt und zum Schluss bleiben nur die Calls zum operator übrig.

    Korrekt. Ändert aber nix.

    Nicht zu verwechseln mit "zum Schluss bleiben nur die Calls zum operator in dem Functiontemplate übrig". Das Functionframe wird entfernt, damit auch noexcept am Funktionskopf.

    Falsch. Das noexcept bleibt natürlich. Wäre auch schlimm wenn es nicht bliebe - würde ja die "as if" Regel verletzen. Frame ist dafür keines nötig, das wird gern über diverse Tables implementiert. Ich gehe davon aus dass du das weisst - weswegen ich mich jetzt ein wenig wundere warum du das Inlining/Entfernen des Frames als Argument anführst.

    #include <iostream>
    #include <sstream>
    
    inline void test2(std::istream& s) noexcept {
        int i;
        s >> i;
        s >> i;
        s >> i;
    }
    
    __attribute__((noinline)) void test(std::istream& s) {
        test2(s);
    }
    
    int main() {
        try {
            std::stringstream s;
            s.exceptions(std::istream::failbit | std::istream::badbit);
            test(s);
        } catch (...) {
            std::cout << "catch" << std::endl;
        }
    }
    

    https://godbolt.org/z/57GqdY8Eo

    test2 wird hier auch vollständig in test inlined. Der Effekt des noexcept bleibt natürlich trotzdem erhalten.

    Output

    terminate called after throwing an instance of 'std::__ios_failure'
      what():  basic_ios::clear: iostream error
    

    Aber wieso diskutieren wir eigentlich immer noch darüber

    Weil du immer wieder versuchst mir was zu erklären, dabei aber falsche und/oder irreführende Dinge schreibst. So wie eben gerade.

    Naja, also es gibt schon Stack-Overflow Exceptions. Es sind nur keine Standard C++ Exceptions und damit ein Implementierungsdetails der Runtime, siehe SEH vs sjlj. Da kann man sich jetzt aber auch prima drüber streiten ob das als eine echte Stack-Overflow Exception durchgeht oder nicht.

    Wäre ein sinnloser Streit. Fakt ist dass es in C++ das Konzept einer Stack-Overflow Exception nicht gibt - es gibt in C++ ja nichtmal das Konzept eines Stack-Overflow. So war das gemeint.

    Ich wollte damit nur sagen, dass solche cleveren Mechanismen nur selten vollständig umgesetzt werden können. Sie hängen oft von Dingen ab, die der C++ Standard nicht hergibt bzw außerhalb des Wirkungsbereichs des Compilers/Runtime liegen

    Der C++ Standard hat ja noexcept(auto) - es ist bloss dummerweise auf implizit definierte spezielle Memberfunktionen eingeschränkt. Ich sehe aber keinen Grund warum es nicht auch für alle Funktionen gehen sollte.

    wie zum Beispiel der berühmte Linux OOM-Killer Kernel Task. Gibts sowas eigentlich auch unter Windows?

    Es ist denkbar dass es ein oder mehrere Third-Party Tools gibt. Ich kenne aber keine, und mit Windows kommt ganz sicher kein solches Tool mit. Windows kommt da besser damit durch, weil Windows Speicher nicht over-committed. Blöd wird's wenn man Adressbereiche hat die automatisch "on first use" committed werden, aber davor nur reserviert sind - wie z.B. Thread-Stacks. Das kann dann in einer STATUS_STACK_OVERFLOW SEH-Exception enden, obwohl der Stack-Pointer noch lange nicht am Ende war. Sehe ich immer wieder in Crash-Dumps. COW kann genau so schief gehen, Schreiben in so einen Bereich kann dann auch in einer SEH enden.

    Das Beispiel ist aber mMn. wieder kein gutes: der OOM-Killer ändert ja nichts am Verhalten des Programms. Er schickt dem Programm bloss ein Signal. Das kann man auch selbst senden. Das Programm wird in beiden Fällen gleich reagieren.



  • @hustbaer sagte in C++20 Tutorial/Howto: Concepts:

    test2 wird hier auch vollständig in test inlined. Der Effekt des noexcept bleibt natürlich trotzdem erhalten.

    Ich war jetzt drauf und drann herumzumotzen, dass normale Funktionen und Templates nicht das Gleiche sind. Also habe ich mal deinen Test mit meinem Tuple Concept aufgebaut und versucht so wenig wie möglich zu ändern. Und weißt du was? Du hast Recht. Und weißt du was mein Fehler war? Mir ist nicht mal im Ansatz in den Sinn gekommen die Exceptions vom Stream einzuschalten und das war echt bescheuert (weißt schon Anfängerlevel). Und ich erzähle noch groß rum, dass man es im Assembleroutput nicht sehen kann... 🤣 Also Entschuldigung wenn ich an der Stelle wirklich engstirnig war. Wer gibt schon gerne so peinliche Fehler zu? Vor allem nach 30+ Jahren Erfahrung mit der Sprache. 😳

    @hustbaer sagte in C++20 Tutorial/Howto: Concepts:

    Aber wieso diskutieren wir eigentlich immer noch darüber

    Weil du immer wieder versuchst mir was zu erklären, dabei aber falsche und/oder irreführende Dinge schreibst. So wie eben gerade.

    Danke dass du drangeblieben bist. Es gibt nur wenige die überhaupt verstehen was da los ist und auch noch bereit sind es zu demonstrieren. Mich kann man immer mit guten Beispielen überzeugen.

    Weißt du was wirklich schlimm an der ganzen Thematik ist? Ich habe im Job mit etwa 20 C/C++ Entwicklern zu tun, und kein einziger davon weiß/versteht über was wir hier reden. Ich "darf" deren Auswüchse in einer 4,5 Millionen Zeilen Codebasis analysieren, fixen und modernisieren. Es ist also mal ganz nett wenn man mal mit jemanden zu tun hat, der die Details kennt.

    @hustbaer sagte in C++20 Tutorial/Howto: Concepts:

    Falsch. Das noexcept bleibt natürlich. Wäre auch schlimm wenn es nicht bliebe - würde ja die "as if" Regel verletzen.

    Ja, die Regel ist mir bekannt. Deswegen war ich selbst auch etwas überrascht, als ich meine ersten Tests gemacht habe. Ich habe die Behauptung ja nicht aufgestellt ohne es vorher auszuprobieren. Aber wenn man die Exceptions nicht einschaltet ... das wird mir noch Tage schlaflose Nächte bereiten.

    @hustbaer sagte in C++20 Tutorial/Howto: Concepts:

    Wäre ein sinnloser Streit. Fakt ist dass es in C++ das Konzept einer Stack-Overflow Exception nicht gibt - es gibt in C++ ja nichtmal das Konzept eines Stack-Overflow. So war das gemeint.

    Ja okay, jetzt ist es klar.

    @hustbaer sagte in C++20 Tutorial/Howto: Concepts:

    Der C++ Standard hat ja noexcept(auto) - es ist bloss dummerweise auf implizit definierte spezielle Memberfunktionen eingeschränkt. Ich sehe aber keinen Grund warum es nicht auch für alle Funktionen gehen sollte.

    Ja, einige der Entscheiden von dem C++ Komitee verstehe ich auch nicht. Wie sie zum Beispiel C++17 uniform initialization so vermasseln konnten. Würden die Compilerentwickler es so implementieren wie vom Komitee vorgeschlagen, könnte man eine Klasse mit privaten Default Constructor trotzdem ohne weiteres mit einer leeren uniform initialization instanzieren. Ich habe jetzt allerdings beim Compiler Explorer keine Compiler Version finden können, wo es nach Vorgabe implementiert (und damit kaputt) ist. Aber es war schon ein fast witziger Bug.

    @hustbaer sagte in C++20 Tutorial/Howto: Concepts:

    Es ist denkbar dass es ein oder mehrere Third-Party Tools gibt. Ich kenne aber keine, und mit Windows kommt ganz sicher kein solches Tool mit. Windows kommt da besser damit durch, weil Windows Speicher nicht over-committed. Blöd wird's wenn man Adressbereiche hat die automatisch "on first use" committed werden, aber davor nur reserviert sind - wie z.B. Thread-Stacks. Das kann dann in einer STATUS_STACK_OVERFLOW SEH-Exception enden, obwohl der Stack-Pointer noch lange nicht am Ende war. Sehe ich immer wieder in Crash-Dumps. COW kann genau so schief gehen, Schreiben in so einen Bereich kann dann auch in einer SEH enden.

    Hmm, interessant. Ich kenne bei Windows nur ein paar wenige spezifische Details, die mich mal irgendwann interessiert haben. Darüber hinaus benutze ich Windows seit 15+ Jahren nicht mehr. Naja abgesehen bis auf diese Spieleentwicklungsgeschichte, die ich weiter oben erwähnt hatte. Aber im Grund ist es auch nur Portierungsarbeit um es zuerst gegen die Wine-lib kompiliert zu bekommen um dann später einen kompletten Linux Port zu machen. Wobei ich denke, dass das nie fertig wird. Man kann bei dem ganzen nicht mal Spaghetticode sprechen, es ist eher Hackfleischcode.

    @hustbaer sagte in C++20 Tutorial/Howto: Concepts:

    Das Beispiel ist aber mMn. wieder kein gutes: der OOM-Killer ändert ja nichts am Verhalten des Programms. Er schickt dem Programm bloss ein Signal. Das kann man auch selbst senden. Das Programm wird in beiden Fällen gleich reagieren.

    Nicht ganz so. Es hängt von den Prozess Capabilities ab. Soweit ich mich erinnern kann, macht der OOM-Killer erst ein SIGTERM wenn CAP_SYS_IORAW (bin mir nicht 100% sicher) gesetzt ist. Damit soll dem Prozess ein sauberer Shutdown ermöglicht werden. Und erst dann ist es ein SIGKILL. Aber ja, im Endeffekt wird nur ein Signal geschickt.



  • @VLSI_Akiko sagte in C++20 Tutorial/Howto: Concepts:

    Also Entschuldigung wenn ich an der Stelle wirklich engstirnig war. Wer gibt schon gerne so peinliche Fehler zu?

    Ja, das kenne ich. Passiert mir auch immer wieder. Also Fehler mach ich - wie jeder Mensch - sowieso öfter. Aber richtig wurmen tut es mich dann, wenn ich erst noch ein paar mal hartnäckig "nachgelegt" habe bevor ich draufgekommen bin dass ich einen Fehler gemacht habe. Aber die peinlichen Fehler sind die einzigen aus denen man lernt. Je weniger peinlich mir ein Fehler ist, desto geringer die Chance dass ich in Zukunft darauf achte ihn zu vermeiden.

    Danke dass du drangeblieben bist.

    Bitte 🙂

    Es gibt nur wenige die überhaupt verstehen was da los ist und auch noch bereit sind es zu demonstrieren. Mich kann man immer mit guten Beispielen überzeugen.

    Weißt du was wirklich schlimm an der ganzen Thematik ist? Ich habe im Job mit etwa 20 C/C++ Entwicklern zu tun, und kein einziger davon weiß/versteht über was wir hier reden. Ich "darf" deren Auswüchse in einer 4,5 Millionen Zeilen Codebasis analysieren, fixen und modernisieren.

    Und genau das ist der Grund warum ich empfehle gewisse Dinge nicht zu tun. Soweit möglich Finger weg von noexcept, von Atomics, von selbst implementierten Containern etc. Diesen 20 Entwicklern (oder anderen wie ihnen) zu empfehlen überall noexcept dranzuschreiben kann mMn. nur nach hinten losgehen.



  • @hustbaer sagte in C++20 Tutorial/Howto: Concepts:

    Ja, das kenne ich. Passiert mir auch immer wieder. Also Fehler mach ich - wie jeder Mensch - sowieso öfter. Aber richtig wurmen tut es mich dann, wenn ich erst noch ein paar mal hartnäckig "nachgelegt" habe bevor ich draufgekommen bin dass ich einen Fehler gemacht habe.

    Ohja, die bereiten einem dann schlaflose Nächte, also mir zumindest. Und dann überlegt man, wie man sich da am besten wieder rauswindet. 🤣 Aber ich muss festellen, dass es zumindest mir mit dem zunehmenden Alter immer leichter fällt, sich solche Fehler einzugestehen. Scheint bei vielen Politikern genau andersrum zu sein. 🤔

    Aber die peinlichen Fehler sind die einzigen aus denen man lernt. Je weniger peinlich mir ein Fehler ist, desto geringer die Chance dass ich in Zukunft darauf achte ihn zu vermeiden.

    Ich lerne aus jedem Fehler, zumindest was Programmierung angeht. Aber ich lege da auch unmenschliche Maßstäbe an mich selbst an, und dann spielen da auch noch andere Fakoren eine Rolle.

    Und genau das ist der Grund warum ich empfehle gewisse Dinge nicht zu tun. Soweit möglich Finger weg von noexcept, von Atomics, von selbst implementierten Containern etc. Diesen 20 Entwicklern (oder anderen wie ihnen) zu empfehlen überall noexcept dranzuschreiben kann mMn. nur nach hinten losgehen.

    Oh keine Sorge, denen zeige ich das nicht. Die bekommen schon einen Krampf wenn ich mal ein explicit, override oder final benutze. Ich musste denen ernsthaft erklären warum dass hier nicht so tut wie sich sich das vorstellen.

    int32_t a = 2000000000;
    int32_t b = 1000000000;
    int64_t c = a + b;
    

    Das mit dem noexcept mache ich schon völlig automatisch da dran, wo es passt. Deswegen auch bei den Beispiel hier. Aber ja, wäre vielleicht vernünftiger es wegzulassen um die Leute nicht erst auf Ideen zu bringen. Hmm, was meinst du? Wäre es eventuell besser, wenn ich auch eine etwas "professionelle" Sprache bei den Beispielen verwende? Habe ja vor noch einige zu machen.



  • @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 unsere SpinLock 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. Und noexcept(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 bei noexcept 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 eher std::array oder std::span anbieten. Ein printAll 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. Bei std::array oder fixed-size std::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 ein catch (...) 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 ein catch (...) 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 die noexcept 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 ein noexcept 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. 😊



  • @VLSI_Akiko

    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 das noexcept von X können davon auch viele noexcept sein und du schreibst es überall dran. Und nun willst du X ändern - und es kann auf einmal nicht mehr noexcept sein. Wodurch jetzt das noexcept 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 viel noexcept 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 Zeiten noexcept 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 weiterhin const 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:

    @VLSI_Akiko

    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 das noexcept von X können davon auch viele noexcept sein und du schreibst es überall dran. Und nun willst du X ändern - und es kann auf einmal nicht mehr noexcept sein. Wodurch jetzt das noexcept 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 viel noexcept 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 Zeiten noexcept 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 weiterhin const zu lassen, trotz Änderungen (z.B. intern synchronisieren)

    Oh, keine Sorge, der Compiler sagt dir auch, wenn das noexcept(false) (also fehlendes noexcept) 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
          |              ^~~~~~~~~~~~~
    


  • @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 der std::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 und baz sind immer noch noexcept, und der Compiler schweigt sich aus. Genaugenommen kann man schon vorher ein Problem bekommen, denn der Compiler schweigt sich auch aus wenn du das noexcept bei foo 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 schliesslich noexcept, 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 ü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.

    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 fehlendes noexcept) 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 garantiert noexcept 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 garantiert noexcept 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 doch Allocator::propagate_on_container_move_assigment. Wenn das std::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 dort std::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 Operationen noexcept 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 noch std::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 halt throw; 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 ein terminate 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 mit bad_allocs beworfen, und Bugs sind ja eher ein Fall für asserts (mag auf magereren Systemen anders aussehen).