C++20 Tutorial/Howto: Concepts



  • So, hiermit starte ich mal meine angekündigte Tutorial/Howto Reihe. Ich werde versuchen ein paar der modernen C++ Features zu erklären, zeigen wie man sie benutzt und warum es (mir) Spaß macht sie zu benutzen. Dieser erste Post in dem Thread wird auch regelmäßig editiert werden, damit er möglichst aktuell und korrekt ist. Ich mache reichlich Fehler und versuche die auch dastehen zu lassen, plus die Korrektur, damit man die Entwicklung erkennen kann. (Manchmal wird es auch hitzig, was man mir hoffentlich verzeihen kann. Ich bin da wirklich mit Leidenschaft dabei. 😁)

    Was sind Concepts?
    Concepts sind Bestandteil der generischen Programmierung seit C++20 und gehören damit zu den Templates. Sie sind eine Erweiterung, die noch mehr Kontrolle und Feintuning von Templates und deren Parameter ermöglichen. Dadurch dass sie Bestandteil der Templates sind, sind sie eine Erweiterung, die zur Compile-Zeit ausgewerten und umgesetzt werden. Sie verursachen also keine zusätzlichen Kosten während der Laufzeit, könnne aber je nach Complexität den Compile-Vorgang verlängern.

    Concepts können auf verschiedene Weisen eingesetzt werden, die aber wohl häufigste ist die Parameter/Typen von Templates stärker einzuschränken. Man kann damit zum Beispiel die restriktiven Datentypen von Ada simulieren, wo man Wertebereiche von Zahlentypen genau definieren kann. Dies ist nun auch mit C++20 auf eine recht einfache/verständliche Art möglich.

    Concept als Typersatz (oder Typengruppe)
    Nehmen wir als Beispiel eine einfache Template Funktionen für ein Multiply-Add (FMA). Dies ist eine häufig in Hardware von DSPs und GPUs umgesetzte Funktion, speziell für Datananalyse (FFT) bzw Bildsynthese (Raytracing, Rastering).

    template <typename T>
    constexpr T multiplyAdd(const T &a, const T &b, const T &c) noexcept
    {
        return a * b + c;
    }
    

    Bei diesem Template kann man allerdings alle möglichen Typen, wie zum Beispiel std::string oder ganze Klassen. Manchmal sind es Typen, bei denen die nötigen Operatoren (operator* und operator+) vorhanden sind, aber ein Multiplay-Add wenig Sinn macht. Da macht es Sinn die Templatefunktion auf Zahlen zu beschränken. Man könnte im Template mit einen static_assert prüfen und einen Fehler während des Kompilierens auslösen. Oder man schreibt ein Concept, mit dem man die möglichen Typen einschränkt, was auch natürlicher/einleuchtender aussieht. Sagen wir mal ich will nur negative und positive, ganze und gebrochene Zahlen zulassen. Also baue ich mir ein Concept names Nummer und zeige auch gleich wie man es als Typ benutzen kann.

    #include <iostream>
    #include <type_traits>
    
    // darf natürlich oder gebrochen sein, aber auf keinen Fall ein bool
    template <typename T>
    concept Number = (std::is_integral_v<T> || std::is_floating_point_v<T>) && !std::is_same_v<T,bool>;
    
    // und so wird es benutzt
    template <Number T>
    constexpr T multiplyAdd(const T &a, const T &b, const T &c) noexcept
    {
        return a * b + c;
    }
    
    int32_t main()
    {
        std::cout << "1*2+3 = " << multiplyAdd(1, 2, 3) << std::endl;
    
        return 0;
    }
    

    Die neuen Testtemplates (Include type_traits) für Typen mit _v am Ende std::is_integral_v<T> sind eine Verkürzung der älteren Testtemplates std::is_integral<T>::value. Diese "Abkürzung" gibt es auch für den Typ, zum Beispiel kann man ein std::underlying_type<T>::type jetzt auch als std::underlying_type_t<T> schreiben. Das noexcept am Ende des Templatefunktionskopfes ist dort, weil nichts in der Funktion passiert, dass eine Exception werfen kann. Es ist ein Hinweis an den Compiler, dass er hier schärfer optimieren kann. Als Anfänger oder auch Fortgeschrittener sollte man es ignorieren, außer man weiß ganz genau, was man tut. Im Zweifel immer weglassen.

    Wenn man zum Beispiel nur positive natürliche Zahlen erlauben will, inklusive bool, kann man es so als Concept umsetzen.

    template <typename T>
    concept UnsignedInteger = std::is_integral_v<T> && std::is_unsigned_v<T>;
    

    Concept als Typbeschränkung
    Concepts können allerdings noch viel mehr, wie zum Beispiel Constraits (Beschränkungen). Hiermit wird etwas möglich, was sehr oft gefragt wird, aber mit den alten C++ Mitteln nur sehr schwer umsetzbar ist. Und zwar wollen Leute wissen ob es möglich ist eine Klasse auf vorhandene Membermethoden zu testen, also Thema Reflektion. Zum Beispiel möchte ich eine Templatefunktion, die nur auf Containern (std::vector, std::list usw) arbeitet, die eine begin() und end() Methode haben.

    #include <iostream>
    
    template <typename T>
    constexpr void printAll(const T &container)
    {
        for (auto it = t.begin(), it != t.end(), ++it)
            std::cout << *it << std::endl;
    }
    

    Das Template kann mit folgendem Concept als Typ eingeschränkt werden.

    #include <iostream>
    #include <type_traits>
    #include <vector>
    
    template <typename T>
    concept Container = requires (T &t){ t.begin(); } && requires (T &t){ t.end(); };
    
    template <Container T>
    constexpr void printAll(const T &container)
    {
        for (auto it = container.begin(); it != container.end(); ++it)
            std::cout << *it << std::endl;
    }
    
    int32_t main()
    {
        std::vector vec{1, 2, 3, 4, 5}; // uniform initialization (C++11) plus erweiterter type deduction (C++17)
    
        printAll(vec);
    
        return 0;
    }
    

    Concept als Reflektion
    Ich arbeite oft und gerne mit Tuples, schränke sie aber gerne etwas ein was ihre Größe oder unterstütze Typen angeht. Zum Beispiel verwende ich gerne ein Tuple-Concept mit ein wenig Tuning um die mögliche Elementanzahl zu beschränken.

    #include <limits>
    #include <tuple>
    #include <type_traits>
    
    template <typename T, size_t MinSize = 1, size_t MaxSize = std::numeric_limits<size_t>::max()>
    concept Tuple = requires (T val){{ std::get<0>(val)}; } &&
                    ((std::tuple_size_v<std::remove_cvref_t<T>> - MinSize) <= (MaxSize - MinSize));
                    // std::remove_cvref_t - erweiterte type_traits (C++20)
    

    Und wie verwendet man das? Ich gebe hier mal ein Beispiel, dass ich so ähnlich in Produktivsystemen einsetze. Das Szenario ist folgendes. Ich arbeite auf den Werten eins Tuples, sprich gehe sequenziell durch die Werte mit einer Schleife und mache dann einfach irgendwas mit den Werten. Sind Schleifen kurz, bietet sich ein Loop-unrolling an, also ein Ausschreiben der Schleife. Loop-unrolls können eine hohen Durchsatz erreichen, weil die Elemente der ehemaligen Schleife hintereinander im Speicher liegen. Für den Prefetcher der CPU (der auf Verdacht die Caches vorfüllt) ist das ein gefundenes Fressen. Loop-unrolling ist eine Compile-Zeit Aktion und man hat dafür oft auf Makros zurückgegriffen. Diese sind aber in C++ unerwünscht, da sie mit der erhöhten Typsicherheit von C++ brechen. Daher gibt es hier ein Loop-unroller für Tuples mit 1 bis 10 Elementen (man muss es begrenzen, damit die Strukturen nicht größer werden als die Caches - Level-1 Cache wohlgemerkt).

    template <Tuple <1,10>T>
    constexpr void printAll(const T &tuple)
    {
        [&]<size_t ...sequence>(std::index_sequence<sequence...>) // anonymes Lambda Template (c++17)
        {
            ((std::cout << std::get<sequence>(tuple) << std::endl), ...); // variadic template parameter pack unpacking (c++11)
        }
        (std::make_index_sequence<std::tuple_size_v<T>>{});
    }
    
    int32_t main()
    {
        std::tuple t{1, 2, 3};
    
        printAll(t);
    
        return 0;
    }
    

    Sieht brutal aus, aber im Endeffekt kommt da hoch optimierter und auch ziemlich kleiner Objectcode raus. Hier verwende ich variadic templates, template parameter unpacking (das (...); in der Mitte) und anonyme Lambdas (die Zeile, die mit [&] beginnt). Ich bin mir 100% sicher, dass es hierzu Fragen gibt. 😅 So sehen zum Beispiel solide Optimierungen in portablen C++20 aus.

    Bei den Beispielen hier verwende ich eine Menge der neuen C++ Features, wie exception-correctness mit den noexcept Modifiern, uniform initializers (vec{...}), erweiterte Typ-deduction für Templates (std::vector ohne expliziten Templateparamter und auto) und constexpr wo es möglich ist. Ich werde die Beispiele und Erklärungen innerhalb dieses Posts erweitern und auf dem Laufenden halten. Falls es Fragen und Anmerkungen gib, nur zu, dafür isses da. 😉



  • Danke für die kurze Einführung hat mir gefallen 🙂



  • Nach dem "Conceps" in der Überschrift, hab ich nicht viel erwartet 😉 , aber mir hat es auch ganz gut gefallen.



  • Vielen Dank für das Tutorial. Es sollte hier im Forum einen abgetrennten Bereich geben indem Mitglieder so etwas veröffentlichen können. Auch mit Anhängen etc.. Kenne das auch aus anderen Foren wo es gut funktioniert.



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

    Nach dem "Conceps" in der Überschrift, hab ich nicht viel erwartet 😉 , aber mir hat es auch ganz gut gefallen.

    Ich werde es noch etwas aufbohren. Ich muss nur vorher immer testen, ob meine Codefragmente auch tun. Concepts in Verbindung mit modernem C++ kann recht "perverse" Auswüchse annehmen, zum Beispiel wenn ich noch variadic templates und parameter unpacking mit reinnehme. ☺



  • 👍



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

    Bei den Beispielen hier verwende ich eine Menge der neuen C++ Features, wie exception-correctnes mit den noexcept Modifiern,

    Ich bin mir nicht sicher was du mit "exception-correctnes" meinst. Klingt aber gefährlich. noexcept ist ein Versprechen, und wenn du es brichst, dann wird terminate aufgerufen. Autsch.
    Daher ist es mMn. am besten noexcept einfach nicht anzugeben, so lange man keinen guten Grund dafür hat.

    Gründe:

    • noexcept in eigenem Code sollte eine "red flag" sein. Wenn man überall noexcept stehen hat, weil man haufenweise noexcept(false) schreibt, desensibilisiert einen das bloss, und man übersieht falsche/unpassende noexcept Angaben leicht.
    • Je öfter man einen noexcept Specifier schreibt, desto mehr Möglichkeiten hat man dabei einen Fehler zu machen. Und da dir der Compiler hier keine Diagnostic gibt, bleibt der dann unerkannt.
    • Gerade in Templates ist noexcept sehr schwer korrekt hinzubekommen.

    Beispiel:

    template <Number T>
    constexpr T multiplyAdd(const T &a, const T &b, const T &c) noexcept
    {
        return a * b + c;
    }
    

    Das noexcept scheint OK. Ist es aber nicht mehr, sobald T z.B. eine BigNum Klasse ist deren Operatoren z.B. Speicher anfordern und daher bad_alloc werfen können.

    Man kann noexcept bewusst/gezielt in einem Programm so einsetzen, dass man etwas mit noexcept markiert, was in bestimmten Fällen Exceptions werfen kann. z.B. wenn eine Operation nur Exceptions wirft in Fällen die man nicht supporten muss, und der Code deutlich einfacher wird wenn man davon ausgehen kann dass es eben nie passiert. Dann schreibt man noexcept dran, und in dem Fall dass so ein "muss-nicht-supported-werden" Szenario eintritt, wird kontrolliert terminate aufgerufen statt dass das Programm z.B. ein falsches Ergebnis ausspuckt.
    Kann man machen. Dann sollte man sich aber sehr genau im Klaren darüber sein wie noexcept funktioniert und gut überlegt haben ob das auch wirklich Sinn macht.

    Anfängern beizubringen möglichst überall noexcept dranzuschreiben ist mMn. auf jeden Fall ein schwerer Fehler.



  • Weiteres Beispiel:

    template <Tuple <1,10>T>
    constexpr void printAll([[maybe_unused]] T &tuple) noexcept
    {
        [&]<size_t ...elements>(std::index_sequence<elements ...>)
        {
            (std::cout << elements << std::endl, ...);
        }
        (std::make_index_sequence<std::tuple_size_v<T>>{});
    }
    

    AFAIK kann man auf std::cout wie auf jedem anderen Stream Exceptions aktivieren. In dem Fall wäre das noexcept wieder gelogen.


    Allgemeiner Rat an alle: Lasst die Finger von noexcept, es sei denn ihr wisst ganz genau wie es funktioniert, und dass und warum ihr es an der Stelle braucht.



  • @Zhavok sagte in C++20 Tutorial/Howto: Conceps:

    Vielen Dank für das Tutorial. Es sollte hier im Forum einen abgetrennten Bereich geben indem Mitglieder so etwas veröffentlichen können. Auch mit Anhängen etc.. Kenne das auch aus anderen Foren wo es gut funktioniert.

    Es gibt zumindest https://www.c-plusplus.net/forum/category/59/die-artikel. Schätze mal Moderatoren verschieben es da rein, wenn es sich lohnt?



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

    Weiteres Beispiel:

    template <Tuple <1,10>T>
    constexpr void printAll([[maybe_unused]] T &tuple) noexcept
    {
        [&]<size_t ...elements>(std::index_sequence<elements ...>)
        {
            (std::cout << elements << std::endl, ...);
        }
        (std::make_index_sequence<std::tuple_size_v<T>>{});
    }
    

    AFAIK kann man auf std::cout wie auf jedem anderen Stream Exceptions aktivieren. In dem Fall wäre das noexcept wieder gelogen.


    Allgemeiner Rat an alle: Lasst die Finger von noexcept, es sei denn ihr wisst ganz genau wie es funktioniert, und dass und warum ihr es an der Stelle braucht.

    Keine Sorge, ich weiß genau wann ich das noexcept verwende. Hier ist mit allerdings ein Fehler durchgerutscht. Muss an der Stelle ganz klar ein noexcept(false) sein. Sobald man auf einem Stream arbeitet, hat man im Normalfall auch Exceptions mit drin. Eventuell solltest du dir mal Scott Meyers Beitrag zu noexcept anschauen, es ist eben nicht so wie es in den meisten Referencen drinsteht. noexcept heißt nicht garantiert keine Exception, sondern höchstwahrscheinlich. Auch die Annahme, dass noexcept wegzulassen oder noexcept(false) zu schreiben, sei das Gleiche, trifft auch nicht zu. Ich arbeite damit schon seit 10+ Jahren und kenne die Fallstricke. Eventuell mach ich auch noch ein Tutorial/Howto zu C++ Gotchas. Das wird garantiert sehr spaßig.

    Und wo wir gerade bei Gotchas waren. Du bist in ein solches hineingelaufen. Dass vor dem tuple Parameter ein maybe_unused steht, hätte dich hellhörig machen sollen. Denn nirgendwo in dem Functiontemplate wird tuple verwendet. Naja, es ist keine Funktion, auch wenn es so aussieht. ☺



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

    Keine Sorge, ich weiß genau wann ich das noexcept verwende.

    Also es macht nicht den Eindruck.

    Hier ist mit allerdings ein Fehler durchgerutscht. Muss an der Stelle ganz klar ein noexcept(false) sein.

    MMn. gehört da, so wie fast überall, einfach gar kein noexcept hin. Eben weil man damit so schnell Fehler macht. Hatte ich aber auch schon geschrieben.

    Eventuell solltest du dir mal Scott Meyers Beitrag zu noexcept anschauen, es ist eben nicht so wie es in den meisten Referencen drinsteht. noexcept heißt nicht garantiert keine Exception, sondern höchstwahrscheinlich.

    Nein. noexcept(true) heisst: Aufrufer dieser Funktion können keine Exceptions verdauen, daher, falls doch eine Fliegt, bitte ruf terminate auf statt die Exception weiter fliegen zu lassen.

    Was Scott Meyers Beitrag angeht: Ich halte viel von Scott Meyers, aber den speziellen Rat "use noexcept wherever you can" halte ich für furchtbar.

    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?

    Und wo wir gerade bei Gotchas waren. Du bist in ein solches hineingelaufen. Dass vor dem tuple Parameter ein maybe_unused steht, hätte dich hellhörig machen sollen. Denn nirgendwo in dem Functiontemplate wird tuple verwendet. Naja, es ist keine Funktion, auch wenn es so aussieht. ☺

    Nein. Liest du auch mal was ich schreibe? Das Tuple interessiert mich nicht.

    Deine Funktion gibt die Indexe des Tuple-Typs mittels std::cout aus.
    Auf std::cout könnten Exceptions aktiviert sein. Das da meine ich: https://www.cplusplus.com/reference/ios/ios/exceptions/
    D.h. der Stream-Insertion Operator könnte eine Exception werfen wenn z.B. std::cout in ein File umgelenkt ist und die Disk voll ist.



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

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

    Keine Sorge, ich weiß genau wann ich das noexcept verwende.

    Also es macht nicht den Eindruck.

    Sicher, alle Beispiele zuvor mit einen std::cout hatten ein noexcept(false) dran. Das Tuple Beispiel war das erste, dass das nicht hatte, weil ich es eben noch schnell eingefügt hatte.

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

    Was Scott Meyers Beitrag angeht: Ich halte viel von Scott Meyers, aber den speziellen Rat "use noexcept wherever you can" halte ich für furchtbar.

    Entspricht aber nicht der Realität. Halbwegs gut gepflegter Code macht eine klare Ansage über das Interface. Schau zum Beispiel mal in den Qt Code, da ist es überall ordentlich definiert.

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

    Nein. Liest du auch mal was ich schreibe? Das Tuple interessiert mich nicht.

    Ja tue ich. Was ich an der Stelle meinte ist, dass durch das Unrolling zum Schluss nur noch die reinen Calls zu den std::cout operator<< übrig bleiben. Das noexcept ist an der Stelle bedeutungslos, kann man in dem Assembler Output sehen.

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

    Deine Funktion gibt die Indexe des Tuple-Typs mittels std::cout aus.

    Aber das ist wohl mein geringstes Problem hier ... uiuiui...

    Auf std::cout könnten Exceptions aktiviert sein. Das da meine ich: https://www.cplusplus.com/reference/ios/ios/exceptions/
    D.h. der Stream-Insertion Operator könnte eine Exception werfen wenn z.B. std::cout in ein File umgelenkt ist und die Disk voll ist.

    Ich weiß, dass std::cout Exceptions werfen kann. Alle Streams können zumindest eine Exception in Verbindung mit dem Stream-state werfen. Aber in einer Beziehung hast du völlig recht. Nur die Indexe des Tuples auszugeben ist ein wenig bescheuert. Wie du siehst, ich mache auch Fehler - reichlich, bescheuert und gelegentlich peinlich. Ist aber auch echt cool, dass jemand genau draufschaut. 😅 Ich sollte echt aufhören existierenden Code auf Minimalbeispiel zusammenschrupfen. Sowas kann echt schiefgehen, wie man hier sieht. Hmm, wie fixe ich es am besten, sodass es ein praktisches Compile-Zeit Beispiel bleibt? 🤔 Ich lass mir was einfallen.


  • Mod

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

    @Zhavok sagte in C++20 Tutorial/Howto: Conceps:

    Vielen Dank für das Tutorial. Es sollte hier im Forum einen abgetrennten Bereich geben indem Mitglieder so etwas veröffentlichen können. Auch mit Anhängen etc.. Kenne das auch aus anderen Foren wo es gut funktioniert.

    Es gibt zumindest https://www.c-plusplus.net/forum/category/59/die-artikel. Schätze mal Moderatoren verschieben es da rein, wenn es sich lohnt?

    Nein, nicht wirklich. Das ist Archiv aus lang vergangenen Tagen, als die Vision für die Seite "Redaktionelle Inhalte mit angehängtem Forum" war. Jetzt ist es primär ein Forum.



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

    Keine Sorge, ich weiß genau wann ich das noexcept verwende. Hier ist mit allerdings ein Fehler durchgerutscht. Muss an der Stelle ganz klar ein noexcept(false) sein. Sobald man auf einem Stream arbeitet, hat man im Normalfall auch Exceptions mit drin. Eventuell solltest du dir mal Scott Meyers Beitrag zu noexcept anschauen,

    Ich verstehe Scott Meyers aber nicht so, dass man überall noexcept oder noexcept(false) dran schreiben sollte, sondern nur noexcept wenn möglich. Abgesehen von wenigen Ausnahmen ist noexcept(false) default.
    Ist mir auch in der Form noch nie über den Weg gelaufen. Bei QT habe ich mir jetzt den Source Code nicht angeschaut, aber zumindest in der Doku konnte ich da auch nichts zu finden.



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

    Ich verstehe Scott Meyers aber nicht so, dass man überall noexcept oder noexcept(false) dran schreiben sollte, sondern nur noexcept wenn möglich. Abgesehen von wenigen Ausnahmen ist noexcept(false) default.

    Ja, hätte ich vielleicht auch nochmal direkt erwähnen sollen. Scott Meyers sagt ganz klar, dass man es verwenden soll, wann immer man kann. Ich schreibe noexcept(false) überall dran (statt es komplett weg zu lassen), weil ich etwas pedantisch bin. Mein Ansatz hier ist: In dem Codeabschnitt besteht die Möglichkeit, dass Exceptions fliegen, weil Strukturen/Funktionen verwendet werden. Weglassen bedeutet für mich "ich weiß es nicht", was eben nicht stimmt. Das meine ich auch mit der klaren Ansage in Interfaces. Ich drücke damit aus, hier können Exceptions fliegen. Wirst auch sehen, dass ich virtual und override immer zusammen verwende, auch wenn man da virtual weglassen kann. Später wirst du auch sehen, dass ich noexcept(noexcept(expr)) verwende, wenn ein Concept Typen zulässt, die nicht exception-safe sind (ganz so wie es in der STL auch gemacht wurde).

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

    Ist mir auch in der Form noch nie über den Weg gelaufen. Bei QT habe ich mir jetzt den Source Code nicht angeschaut, aber zumindest in der Doku konnte ich da auch nichts zu finden.

    Ja, in der Doku und der Reference von Qt ist es nicht extra vermerkt. Aber wenn man zum Beispiel die Qt Quelltexte durchsuchen muss, weil bis heute das QLayout in deren Fusion Theme QGroupBox oben zwischen Überschrift und Content Bereich 6-12 Pixel abschneidet, fällt einem das schnell auf. (Okay, ich weiß nicht ob das in Qt6 gefixt ist, aber der Scheiß ist in allen Qt5 Versionen.) Richtig auffällig wird es, wenn du in die Qt Komponenten schaust, wo Performance eine große Rolle spielt. Zum Beispiel die Vector/Point und Matrix Implementierungen. Ansonsten kann man sich hier auch mal die STL Implementierungen (libstd++ und libc++, keine Ahnung wie es in den Visual Studio aussieht), insbesondere die C++11 und spätere Komponenten, anschauen. Da ist es konsequent umgesetzt.



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

    Ich schreibe noexcept(false) überall dran (statt es komplett weg zu lassen), weil ich etwas pedantisch bin.
    [...]
    Später wirst du auch sehen, dass ich noexcept(noexcept(expr)) verwende, wenn ein Concept Typen zulässt, die nicht exception-safe sind (ganz so wie es in der STL auch gemacht wurde).

    Gepaart mit Pedanterie kann das ganz schnell zum sprichwörtlichen "Rabbit Hole" werden. Man kann ja durchaus endlos lange noexcept(noexcept(expr))-Ausdrücke hinschreiben, die die gesamte Funktion nachzeichnen 😉 ... an den Punkt kam ich mal, als ich dachte "das ist ja ne tolle Sache". Seitdem beschränke ich mich bei noexcept hauptsächlich auf Konstruktoren (Container wie std::vector können da stellenweise effizientere Operationen durchführen, siehe std::move_if_noexcept) und kompakte (generische) Funktionen, die darauf ausgelegt sind, ohnehin nur in wenige Instruktionen zu zerfallen, wenn der Compiler mit ihnen durch ist.

    Man kann ja von Java halten was man mag, aber dieses Propagieren von Exception-Spezifikationen ist schon ne praktische Sache. Zumindest könnte der C++-Compiler ja vielleicht irgendwann mal ein implizites noexcept generieren, wenn in der Funktion keine Ausdrücke auftreten, die nicht noexcept sind - falls das nicht intern aus Optimierungsgründen ohnehin schon gemacht wird.



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

    Geppart mit Pedanterie kann das ganz schnell zum sprichwörtlichen "Rabbit Hole" werden. Man kann ja durchaus endlos lange lange noexcept(noexcept(expr))-Ausdrücke hinschreiben, die die gesamte Funktion nachzeichnen 😉 ... an den Pinkt kam ich mal, als ich dachte "das ist ja ne tolle Sache".

    Hrhr, ich weiß gaaanz genau was du meinst. Ich höre bloß nicht auf und ziehe es bis zum bitteren Ende durch. Nein, es macht mir einen perversen Spaß. 😂

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

    Man kann ja von Java halten was man mag, aber dieses Propagieren von Exception-Spezifikationen ist schon ne praktische Sache.

    Also wenn ich über Java meckere, dann über das alte, was ich zu Studienzeiten lernen musste. Ein aktuelles Java ist durchaus okay und hat wie du schon sagst, durchaus brauchbare Features.

    Zumindest könnte der C++-Compiler ja vielleicht irgendwann mal ein implizites noexcept generieren, wenn in der Funktion keine Ausdrücke auftreten, die nicht noexcept sind - falls das nicht intern aus Optimierungsgründen ohnehin schon gemacht wird.

    Ja, wäre echt klasse. Ist aber nicht so einfach. Viele dieser Geschichten, auch in Verbindung mit trivial Typen, hängen von Heap und Stack Eigenschaften ab, und diese variieren von Platform zu Platform und OS zu OS. Und wie eklig das werden kann, sieht man an den verschiedenen Datenmodellen die von Betriebssystemen verwendet werden. Ein Linux auf AMD64 hat zum Beispiel ein LP64 Datenmodell, ein Windows auf AMD64 hat ein LLP64 Datenmodell und ein Unicos auf den guten alten Cray Mainframes war ein irres SILP64 Datenmodell.

    LLP64: long long, pointer, size_t = 64 Bit
    LP64: long, long long, pointer, size_t = 64 Bit
    SILP64: short, int, long, long long, pointer, size_t = 64 Bit

    Deswegen sind 64 Bit kompatible Codeportierungen zwischen Linux und Windows, wenn da Pointer zu longs gecastet werden, ein echter Krampf. Selbst unter Windows sind 32 -> 64 Bit Portierungen unerträglich, weil zu 32Bit Zeiten von "Idioten" longs für Pointerspielereien benutzt wurden, anstatt size_t/ssize_t oder besser ptrdiff_t. Ich komme damit immer wieder in Berührung weil ich zu den Entwicklern gehöre, die freiweillig an Neocron Classic arbeiten. Das sind 1,4 Millionen Zeilen gigantischer Blödsinn (stellweise pre C++98 Code).



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

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

    Was Scott Meyers Beitrag angeht: Ich halte viel von Scott Meyers, aber den speziellen Rat "use noexcept wherever you can" halte ich für furchtbar.

    Entspricht aber nicht der Realität. Halbwegs gut gepflegter Code macht eine klare Ansage über das Interface. Schau zum Beispiel mal in den Qt Code, da ist es überall ordentlich definiert.

    Was soll "entspricht nicht der Realität" heissen? Das ist (m)eine Meinung. Ich finde z.B. auch dass JavaScript eine furchtbare Programmiersprache ist. Das ist auch eine Meinung. Ist die jetzt auch "falsch" weil so viele Leute JavaScript nutzen? Nein.

    Davon abgesehen...

    Bei bestimmten Klassen in Libraries, speziell Container Klassen oder low-level Utilities wie Mutexen, Scoped-Lock etc., ist noexcept sinnvoll. Damit der Benutzer die Möglichkeit hat diese in den paar wenigen noexcept Funktionen die er selbst schreiben muss/möchte zu verwenden. Bzw. ganz speziell trifft es auch auf Copy- und Move-Konstruktoren und Zuweisungsoperatoren zu, denn hier kommt die Compiler-Magie zum Einsatz die bei implizit definierten solchen das noexcept automatisch passend wählt - abhängig vom noexcept der Basis-/Memberklassen.

    Davon abgesehen lässt man es lieber sein. Ich habe schon einiges an Code gesehen wo jmd. noexcept nutzt. Aber, abgesehen von trivialen Beispielen und Libraries deren Code von tausenden anderen Programmierern angesehen wird, noch nichts wo noexcept durchgängig korrekt gewesen wäre. Ich meine mich sogar zu erinnern dass in einer C++ Standard Library Spezifikation ein noexcept Fehler drin war.

    Und da die Fehlerrate so hoch ist, der Nutzen in normalen Anwendungscode so gering und die Folgen "drastisch" (terminate()) ist und bleibt meine Empfehlung ganz klar: lasst von noexcept die Finger, es sei denn ihr wisst ganz genau warum und wofür ihr es an einer Stelle braucht. Leuten zu empfehlen noexcept zu verwenden, wenn man quasi sicher weiss dass es viele oft falsch verwenden werden, macht mMn. einfach keinen Sinn.

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

    Nein. Liest du auch mal was ich schreibe? Das Tuple interessiert mich nicht.

    Ja tue ich. Was ich an der Stelle meinte ist, dass durch das Unrolling zum Schluss nur noch die reinen Calls zu den std::cout operator<< übrig bleiben. Das noexcept ist an der Stelle bedeutungslos, kann man in dem Assembler Output sehen.

    Ist doch egal ob nach dem Unrolling nur noch std::cout operator<< übrig bleibt, der reicht ja schliesslich.

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

    Deine Funktion gibt die Indexe des Tuple-Typs mittels std::cout aus.
    Auf std::cout könnten Exceptions aktiviert sein. Das da meine ich: https://www.cplusplus.com/reference/ios/ios/exceptions/
    D.h. der Stream-Insertion Operator könnte eine Exception werfen wenn z.B. std::cout in ein File umgelenkt ist und die Disk voll ist. Aber das ist wohl mein geringstes Problem hier ... uiuiui...

    Bitte beim Quoten aufpassen. Der Satz "Aber das ist wohl mein geringstes Problem hier ... uiuiui..." ist nicht von mir.

    Ich weiß, dass std::cout Exceptions werfen kann.

    OK. Wieso schreibst du dann

    Was ich an der Stelle meinte ist, dass durch das Unrolling zum Schluss nur noch die reinen Calls zu den std::cout operator<< übrig bleiben. Das noexcept ist an der Stelle bedeutungslos, kann man in dem Assembler Output sehen.

    ?
    Das widerspricht sich doch.



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

    Zumindest könnte der C++-Compiler ja vielleicht irgendwann mal ein implizites noexcept generieren, wenn in der Funktion keine Ausdrücke auftreten, die nicht noexcept sind - falls das nicht intern aus Optimierungsgründen ohnehin schon gemacht wird.

    Ja, wäre echt klasse. Ist aber nicht so einfach. Viele dieser Geschichten, auch in Verbindung mit trivial Typen, hängen von Heap und Stack Eigenschaften ab, und diese variieren von Platform zu Platform und OS zu OS. Und wie eklig das werden kann, sieht man an den verschiedenen Datenmodellen die von Betriebssystemen verwendet werden.
    (...)

    Für noexcept(auto) spielt das alles keine Rolle. Gibt in C++ schliesslich keine Exceptions für Dinge wie Stack-Overflow, falsches Alignment etc. Das ist alles UB und wird wenn überhaupt, dann über Mittel die sich ausserhalb des C++ Standards befinden behandelt.



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

    Was soll "entspricht nicht der Realität" heissen? Das ist (m)eine Meinung. Ich finde z.B. auch dass JavaScript eine furchtbare Programmiersprache ist. Das ist auch eine Meinung. Ist die jetzt auch "falsch" weil so viele Leute JavaScript nutzen? Nein.

    Ah okay, dann habe ich es in den falschen Hals bekommen. Meine Empfehlung ist die, die auch Scott Meyers gibt, dranmachen wenn möglich. Da wo man es quasi bedenkenlos machen kann sind Getter, die nur einen trivialen member Typ zurückgeben.

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

    Davon abgesehen lässt man es lieber sein. Ich habe schon einiges an Code gesehen wo jmd. noexcept nutzt. Aber, abgesehen von trivialen Beispielen und Libraries deren Code von tausenden anderen Programmierern angesehen wird, noch nichts wo noexcept durchgängig korrekt gewesen wäre.

    Naja, Beispiel Qt Code, wie schon erwähnt. Ich sehe es oft in gut gepflegten Codebasen in Firmen (also kein OpenSource). Durchgängig korrekt ist es selten, aber zu sagen wir mal gut 95%. Gilt natürlich nur für das, was ich bisher an Code gesehen habe.

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

    Bitte beim Quoten aufpassen. Der Satz "Aber das ist wohl mein geringstes Problem hier ... uiuiui..." ist nicht von mir.

    Ohja, entschuldige, das war keine Absicht.

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

    Was ich an der Stelle meinte ist, dass durch das Unrolling zum Schluss nur noch die reinen Calls zu den std::cout operator<< übrig bleiben. Das noexcept ist an der Stelle bedeutungslos, kann man in dem Assembler Output sehen.
    ?
    Das widerspricht sich doch.

    Diesmal hast du nicht genau gelesen. Es wird alles entrollt und zum Schluss bleiben nur die Calls zum operator übrig. 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. Du kannst es nicht nur im Assembleroutput sehen, du kannst es auch kompilieren, eine Exception provozieren und du wirst sehen, terminate() wird erst nach dem Stackunrolling/Exceptionhandler aufgerufen, wie als hätte da nie ein noexcept gestanden. Aber wieso diskutieren wir eigentlich immer noch darüber wobei ich da doch schon vor einer gefühlten Ewigkeit gesagt hatte, dass es falsch war und da ohne Zweifel ein noexcept(false) dran gehört? Dicht gefolgt von meiner Erklärung zu meiner Vorgehensweise "sind Streams beteiligt, können Exceptions fliegen".

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

    Für noexcept(auto) spielt das alles keine Rolle. Gibt in C++ schliesslich keine Exceptions für Dinge wie Stack-Overflow, falsches Alignment etc. Das ist alles UB und wird wenn überhaupt, dann über Mittel die sich ausserhalb des C++ Standards befinden behandelt.

    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. Aber ja, ist außerhalb des C++ Standards, da hast du völlig Recht. ☺ 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, wie zum Beispiel der berühmte Linux OOM-Killer Kernel Task. Gibts sowas eigentlich auch unter Windows?


Log in to reply