Template, Concepts ok?



  • Moin,

    ich habe folgende Funktion geschrieben mit welcher man zwei Vektoren vereinigen kann.

    template<typename T>
    concept UnionContConcept = requires(T a)
    {
        { a.size() } -> std::convertible_to<std::size_t>;
        { a.resize(static_cast<size_t>(42)) };
        { std::random_access_iterator<decltype(std::begin(a))> };
    };
    
    
    /**
    *******************************************************************************
    *	\FUNCTION void AlgExt_Union(Cont& M1, size_t Index, const Cont& M2)
    *
    *	\DESCRIPTION
    *		Fügt die Menge M2 ab der Stelle Index in M1 ein.
    * 
    *       Abhängig von der Größe von M2 und dem Index, überschreibt die Funktion 
    *       Werte in M1 oder fügt diese M1 hinzu.
    * 
    *       Beispiele:
    *
    *       |             M1              | Index |     M2    |          Ergebnis (M1)      |
    *       |-----------------------------|-------|-----------|-----------------------------|
    *       | (1, 2, 3)                   |   0   | (9)       | (9, 2, 3)                   |
    *       | (1, 2, 3)                   |   4   | (2)       | (1, 2, 3, 2)                |
    *       | (1, 2, 3)                   |   1   | (4, 5, 6) | (1, 4, 5, 6)                |
    *       | (1, 2, 3, 4, 5, 6, 7, 8, 9) |   1   | (9 9)     | (1, 9, 9, 4, 5, 6, 7, 8, 9) |
    *******************************************************************************
    **/
    template<UnionContConcept Cont>
    void AlgExt_Union(Cont& M1, size_t Index, const Cont& M2)
    {
        if (Index < M1.size())
        {
            if (Index + M2.size() > M1.size())
                M1.resize(Index + M2.size());
            std::ranges::copy(M2, std::begin(M1) + Index);
        }
        else
        {
            std::ranges::copy(M2, std::back_inserter(M1));
        }
    }
    

    Da ich noch nicht fit in Sachen Concepts bin, möchte ich deswegen mal nachfragen, ob es hierzu Verbesserungsvorschläge gibt.



  • @Quiche-Lorraine sagte in Template, Concepts ok?:

    template<typename T>
    concept UnionContConcept = requires(T a)
    {
    { a.size() } -> std::convertible_tostd::size_t;
    { a.resize(static_cast<size_t>(42)) };
    { std::random_access_iterator<decltype(std::begin(a))> };
    };

    Hey, das ist noch ein wenig holprig.

    template<typename T>
    concept UnionContConcept = requires(T a, std::size_t size)
    {
        { a.size() } -> std::convertible_to<std::size_t>;
        { a.resize(size) };
        requires std::ranges::random_access_range<T>;
    };
    

    oder alternativ

    template<typename T>
    concept UnionContConcept = std::ranges::random_access_range<T>
        && std::ranges::sized_range<T>
        && requires(T a, std::size_t size) { { a.resize(size) }; };
    

    Sry, dass ich so kurz angebunden bin aber wenn du Fragen hast, dann beantworte ich die gerne. 🙂



  • Ich hätte andere Anmerkungen:

    Aus
    concept UnionContConcept
    hätte ich
    concept UnionContainer
    gemacht.
    Grund: Bei "Cont" war mir nicht sofort klar, was du meinst. Cont = Content? Continue? Von daher sehe ich Grund dafür, das auszuschreiben. Ist es vielleicht sogar ein "UnionableContainer"? Klingt komisch, vielleicht "MergeableContainer"?

    Sind es überhaupt "Container"? Im Docstring steht was von "Fügt die Menge M2 ab der Stelle Index in M1 ein." - aber um Mengen handelt es sich keinesfalls, denn dann würde ja sowas wie std::set Sinn ergeben - Mengen haben auch keine Ordnung (und keine Dopplungen). Daher ist das Wording auf jeden Fall verbesserbar. Dein Code funktioniert ja gerade nicht mit Mengen. Von daher wäre vielleicht das Wort "Sequence" besser geeignet. -> concept MergableSequence? Gutes Naming ist schwierig!

    if (Index + M2.size() > M1.size())
    Bei solchem Code habe ich immer Angst vor wrap around der Addition.

    M1.resize(Index + M2.size()); - resize legt dir alle Elemente erstmal an (erfordert u.a. einen Default-Konstruktor). Je nach dem, wie teuer das ist, wäre vielleicht reserve eine Option nebst Änderung des Codes, sodass das alles wieder passt?



  • @Quiche-Lorraine @DNKpp Funktioniert das Constraint{ a.resize(size) } eigentlich mit allen erlaubten Size-Typen? Das muss ja nicht unbedingt ein size_t sein, und dass der Typ "convertible to" size_t ist, heißt ja nicht zwangsläufig, dass das auch umgekehrt gilt (obwohl so ein Typ wohl recht exotisch sein müsste). Ich frage mich gerade, ob es theoretisch auch einen sinnvollen Typen geben kann, wo das nicht greift. 8- und 16-bit Integer-Typen sollten eigentlich gehen, aber ich hätte das wahrscheinlich generischer als decltype(a.size()) formuliert. macht natürlich den Code unübersichtlicher und es ist fraglich, ob man es wirklich braucht. Ich hab mich selbst schon öfter in eigenem Code verlaufen, der einfach zu generisch war 😉

    Achja, und std::ranges::copy und std::back_inserter sind soweit ich weiss constexpr (ebensostd::vector und seine Funktionen ab C++20 ), da wäre es fein, wenn die Union-Funktion das ebenfalls sein könnte.



  • @Finnegan sagte in Template, Concepts ok?:

    Funktioniert das Constraint{ a.resize(size) } eigentlich mit allen erlaubten Size-Typen?

    Die Frage ist, hast du Container für die das nicht der Fall ist? Ich kenne den use-case nicht. Wenn es Unterschiede gibt, dann wäre es durchaus eine Möglichkeit Container::size_type statt plain std:size_t zu verwenden, oder den return von .size() zu ermitteln. Wenn das nicht einheitlich verfügbar ist, dann muss man sich wohl eine trait-struktur basteln.
    Wenn man hinter alle diese Annahmen ein Fragezeichen stellt, dann wird das sehr schnell sehr komplex.

    • Was ist, wenn es kein .size() gibt sondern .get_size()?
    • Oder .resize() heißt .set_size()?

    Concepts sind nicht dazu da, alle möglichen Fälle zu abzudecken, sondern um constraints einheitlich abfragen zu können. Wenn diese von dem Typen nicht erfüllbar sind, dann funktioniert auch deine Funktion nicht, für die du das Concept entworfen hast.

    Jedenfalls bezieht std::convertible_to explizite Konvertierungen mit ein, daher müsste der Type wohl sehr exotisch sein, dass das nicht funktioniert.



  • @DNKpp sagte in Template, Concepts ok?:

    @Finnegan sagte in Template, Concepts ok?:

    Funktioniert das Constraint{ a.resize(size) } eigentlich mit allen erlaubten Size-Typen?

    Die Frage ist, hast du Container für die das nicht der Fall ist? Ich kenne den use-case nicht. Wenn es Unterschiede gibt, dann wäre es durchaus eine Möglichkeit Container::size_type statt plain std:size_t zu verwenden, oder den return von .size() zu ermitteln. Wenn das nicht einheitlich verfügbar ist, dann muss man sich wohl eine trait-struktur basteln.

    Nein, der Einwand ist nur theoretischer Natur. std::size sieht z.B. auch so aus: constexpr auto size( const C& c ) -> decltype(c.size());. Intuitiv hätte ich das also erstmal generisch gehalten. Daher frage ich ja 😉

    Jedenfalls bezieht std::convertible_to explizite Konvertierungen mit ein, daher müsste der Type wohl sehr exotisch sein, dass das nicht funktioniert.

    Denke ich auch. Sich auf den von a.size() zurückgegebenen Typen zu beziehen empfinde ich allerdings intuitiv erstmal als generischer. Aber da greift das Argument mit dem extra Code für wenig konkreten Nutzen. Wenn es aber doch etwas geben sollte, das einem erstmal nicht einfällt, dann wäre man damit dennoch fein raus 😉



  • @All

    Danke für eure Tips bzw. Diskussion.

    @DNKpp:

    Danke für dein Angebot.

    Ich bin da noch dabei die einzelnen Elemente zu verstehen. Rein gedanklich verstehe ich std::ranges::random_access_range bzw. std::random_access_iterator. Etwas verwundert bin ich aber über die Definition aus https://en.cppreference.com/w/cpp/iterator/random_access_iterator.

    Da steht z.B. std::totally_ordered<I>. Das sieht so aus als wäre I der Iteratortyp. Aber wie leitet der Compiler diesen her?

    #include <vector>
    #include <iterator>
    #include <list>
    
    
    template<typename T>
    concept FooConcept = std::ranges::random_access_range<T>;
    
    class ABC
    {
    public:
        double x = 0;
        double y = 0;
        double z = 0;
    
    
        bool operator<(const ABC&) const = delete;
    };
    
    template<FooConcept T>
    void Test(const T&)
    {
    
    }
    
    // Testprogramm
    int main() {
        std::vector<ABC> L1 { {1, 2, 3} };
        std::list<ABC> L2 { {1, 2, 3} };
    
        Test(L1);
        Test(L2);       // Verstehe ich, wird von Intellisense markiert
        return 0;
    }
    

    @wob

    Gutes Naming ist schwierig!

    Oh ja, das Problem kenne ich zur Genüge.

    Ich bin da noch etwas unentschlossen. MergableSequence erinnert mich irgentwie an std::merge.



  • @Quiche-Lorraine sagte in Template, Concepts ok?:

    @DNKpp:
    Danke für dein Angebot.
    Ich bin da noch dabei die einzelnen Elemente zu verstehen. Rein gedanklich verstehe ich std::ranges::random_access_range bzw. std::random_access_iterator. Etwas verwundert bin ich aber über die Definition aus https://en.cppreference.com/w/cpp/iterator/random_access_iterator.
    Da steht z.B. std::totally_ordered<I>. Das sieht so aus als wäre I der Iteratortyp. Aber wie leitet der Compiler diesen her?

    Was genau meinst du mit "Aber wie leitet der Compiler diesen her?"
    Wunderst du dich, wie der Compiler den Iterator-Typ des Containers ermittelt? I ist an dieser Stelle nichts weiter als eine Template-Argument, welches das Concept verlangt.

    template< class T >
    concept random_access_range = ranges::bidirectional_range<T> && std::random_access_iterator<ranges::iterator_t<T>>;
    

    https://en.cppreference.com/w/cpp/ranges/random_access_range

    Wie du siehst, extrahiert random_access_range mittels des std::ranges::iterator traits den Iterator-Typ, welches intern decltype(ranges::begin(std::declval<T&>())) benutzt.



  • @DNKpp sagte in Template, Concepts ok?:

    Wunderst du dich, wie der Compiler den Iterator-Typ des Containers ermittelt?

    Genau



  • @DNKpp

    Danke für deine Hilfe! Habs verstanden.

    Habe vergessen std::ranges::random_access_range genau anzuschauen und daher dies übersehen.



  • @Quiche-Lorraine sagte in Template, Concepts ok?:

    @wob

    Gutes Naming ist schwierig!

    Oh ja, das Problem kenne ich zur Genüge.
    Ich bin da noch etwas unentschlossen. MergableSequence erinnert mich irgentwie an std::merge.

    Ja, ich war mit MergableSequence auch nicht zufrieden. Gibt es überhaupt Sequences, die nicht mergeable sind? Von daher müsste vielleicht irgendwas rein, weswegen du dieses Concept überhaupt machst. Bzw. irgendwie sollte so klar wie möglich sein, was dieses Concept eigentlich erreicht. Was ist es mehr als ein Container mit Random Access Iterator?

    Für wichtige Namen, die wir sogar mal im Team diskutiert haben (da muss man dann wieder aufpassen, dass man nicht zu viel Zeit versenkt), habe ich dann sogar mal ChatGPT gefragt und tatsächlich auch mal gute Ideen bekommen, an die niemand im Team gedacht hatte, die aber eigentlich viel besser und klarer waren. Und nachdem wir dann ein größeres Rename gemacht hatten, fiel sofort schon vom sprachlichen her ein Programmierfehler auf!

    Aber diese Diskussion führt ja an dem eigentlichen Problem etwas vorbei, ich wurde nur vom Begriff der "Menge" getriggert 🙂


Anmelden zum Antworten