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 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).



  • @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 ein terminate 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. nach stderr schreiben. Das ist dann wirklich kaum besser als das was die C++ Runtime Library machen würde - die schreibt typischerweise auch ne Meldung nach stderr 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 doof 🙂

    Ich 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 bei std::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 bei std::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.



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

    Ich kann die fatalistische Einstellung gegenüber bad_alloc die viele Entwickler haben (darunter leider auch viele Library-Entwickler) auf jeden Fall nicht ganz verstehen.

    Ja, die "großen" Anforderungen bei bestimmten extern ausgelösten Aktionen sind ein gutes Argument. Ich werd das nochmal überdenken. Ich hatte hauptsächlich den Fitzelkram im Sinn, den jedes Programm permanent macht, 100 Byte für nen std::string hier, 4k für nen keinen std::vector-Buffer da. Ich denke die meisten Funktionen in beliebigem Code, die bad_alloc werfen, werden eher in diesem Kontext verwendet. Wenn da nen bad_alloc kommt, geht meist eh nicht viel mehr, als würdevoll mit ner Fehlermeldung auszusteigen - und das könnte auch Code im Umkreis von terminate erledigen.

    Vielleicht sind ja noexcept-Allokationen per Default eine Idee und die Exception-Varianten fordert man explizit an, z.B. wenn man eine Datei komplett in den Speicher laden oder anderes Sperrgut verarbeiten will. Derzeit ist es ja umgekehrt... das ist aber außerhalb eigener Bibliotheken eh ein Ding für die weitere Evolution der Standardbibliothek.



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

    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).

    Programme, die Nutzereingaben bearbeiten, können sehr schnell bad_allocs bekommen, weil einfach die Nutzereingaben nicht passen. Allerdings ist in Zeiten von memory overcommitment das ganze Procedere ohnehin sehr zweifelhaft, da man kein bad_alloc bekommt sondern im Regelfall ein seg_fault beim Zugriff auf die Speicherseite, die nicht angelegt werden kann.



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

    24 chars? -> aka small string optimization

    Unterschiedlich je nach compiler.



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

    Vielleicht sind ja noexcept-Allokationen per Default eine Idee und die Exception-Varianten fordert man explizit an, z.B. wenn man eine Datei komplett in den Speicher laden oder anderes Sperrgut verarbeiten will.

    Ohne zusätzliche Sprachfeatures wird das denke ich nix. Ich meine, so Sachen wir Strings-Konkatenieren oder mal schnell ein push_back auf nen vector machen ist schon sehr praktisch. Oder Memory-Streams. Oder ca. 1'000'000 andere Dinge die dynamisch Speicher anfordern. Wenn man da überall manuell prüfen muss ob es geklappt hat... pfuh. Mühsam.



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

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

    Vielleicht sind ja noexcept-Allokationen per Default eine Idee und die Exception-Varianten fordert man explizit an, z.B. wenn man eine Datei komplett in den Speicher laden oder anderes Sperrgut verarbeiten will.

    Ohne zusätzliche Sprachfeatures wird das denke ich nix. Ich meine, so Sachen wir Strings-Konkatenieren oder mal schnell ein push_back auf nen vector machen ist schon sehr praktisch. Oder Memory-Streams. Oder ca. 1'000'000 andere Dinge die dynamisch Speicher anfordern. Wenn man da überall manuell prüfen muss ob es geklappt hat... pfuh. Mühsam.

    Die Idee ist ja gar nicht zu prüfen - exceptions sind schon klasse, das soll jetzt kein zurück-zu-errorcodes-Vorschlag sein. Der Allocator ruft dann im Fehlerfall einfach terminate auf, wenn er für noexcept konfiguriert wurde. Und wenn nicht, dann bleibt alles beim alten.

    if constexpr (if_allocator_is_configured_for_noexcept<Alloc>)
        terminate();
    else
        throw ...
    

    oder noch banaler: die Funktion bekommt ein noexcept und wirft trotzdem.

    Lediglich das vector::push_back und andere müssten zusätzlich einen Specifier á la noexcept(allocator_is_configured_for_noexcept<Alloc>) bekommen. Sollte eigentlich ohne neue Sprachfeatures gehen. So zumindest meine naiv-spontane Vorstellung 😉 ... übersehe ich was (abgesehen von all dem Legacy-Code der meist nicht gut auf Änderung von Defaults reagiert) ?



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

    Unterschiedlich je nach compiler.

    Nope, das ist ein Implementierungsdetail der C++ Lib und ist nicht wirklich Bestandteil des Compilers. Genau genommen ist es ein Detail der STL seit C++98.



  • @VLSI_Akiko Von mir aus nenn es Implemtierung der Stl - es war klar (sollte klar gewesen sein), was gemeint ist.



  • @Finnegan Ach so meinst du das. @VLSI_Akiko hatte vorhin new (std::nothrow) erwähnt, daher hatte ich das im Kopf und dachte du beziehst dich darauf.

    Ja, es gibt auch einige Projekte die operator new überschreiben und darin terminate aufrufen statt zu werfen. Oder C Projekte die bewusst auf Fehlerchecks bei malloc verzeichten "weil's eh crasht" wenn man den Zeiger dann dereferenziert.

    operator new überschreiben ist etwas, was ich auch noch irgendwo OK finde. Also wenn die Anwendung das macht, nicht wenn es irgendein bekifftes Framework macht das sich für etwas zu wichtig hält. Für manche Anwendungen ist das vermutlich völlig OK. Wenn man dann davon profitieren möchte, müsste man natürlich Änderungen an der Standard Library machen.

    noexcept(allocator_is_configured_for_noexcept<Alloc>)

    Etwas ala noexcept(noexcept(_MyAlloc.allocate(1)) && other_conditions) sollte ausreichend sein.



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

    @Finnegan Ach so meinst du das.

    Ja, mir gehts vornehmlich darum, dass man damit wahrscheinlich eine reiche noexcept-Ernte einfahren könnte. Ich hab jetzt keine belastbaren Zahlen, aber mein Bauchgefühl sagt mir, dass gerade bei bad_alloc besonders viele Funktionen noexcept werden könnten, die auch noch in besonderem Maße davon profitieren würden - ich behaupte mal, dass Allokationen gerade in Konstruktioren besonders häufig sind.

    noexcept(allocator_is_configured_for_noexcept<Alloc>)`

    Etwas ala noexcept(noexcept(_MyAlloc.allocate(1)) && other_conditions) sollte ausreichend sein.

    Nun, ich habe allocator_is_configured_for_noexcept<Alloc> ja nicht definiert. Das kann ja durchaus template <typename Alloc> concept allocator_is_configured_for_noexcept = noexcept(_MyAlloc.allocate(1)) && other_conditions; oer sowas sein ... ich glaube nicht, dass man das für jede Funktion alles hinschreiben wollte 😉



  • Der && other_conditions Teil wird ja nicht bei jeder Funktion gleich sein 😉

    Ich wollte damit nur darauf hinweisen dass man die Allokatoren nicht extra irgendwie "taggen" muss dass sie noexcept sind. Es würde einfach reichen sie noexcept zu machen.

    Ich hab jetzt keine belastbaren Zahlen, aber mein Bauchgefühl sagt mir, dass gerade bei bad_alloc besonders viele Funktionen noexcept werden könnten, die auch noch in besonderem Maße davon profitieren würden - ich behaupte mal, dass Allokationen gerade in Konstruktioren besonders häufig sind.

    Ja, klar. Die C++ Standard Library macht ja ausser Speicheranforderungen kaum Dinge die schief gehen können. Die grossen Gruppen sind:

    • bad_alloc
    • Falsche Argumente (vector::at & Co)
    • IO Fehler (std::filesystem & Co)

    Bis auf IO Fehler könnte man alles in vielen Programmen noexcept erklären und das Programm im Fall des Falles einfach abbrechen lassen.



  • Ich habe mal noch ein Beispiel für Concepts + variadic Templates ergänzt. Hierfür findet man erstaunlich wenige Information wie man das korrekt als Constraints verwendet.