Und mal wieder variadische Templates...



  • Also Listen als eigenständige Bestandteile der Sprache. Das könnte auch interessant sein.

    Nun, da klingelt was ... List?, List operation?, List processor?, Lispler?, ach ich komme nicht drauf. 🙂

    Dann gabs da so einen akademischen Ableger, mit so Macroexpansion und Ellipsen ... ich glaube der hiess Schelm? 🙂




  • Mod

    höhenkoller schrieb:

    std::cout << "{ " << (args << ' ' <<)... << "}\n"; // Geht sowas ohne Workaround?
    

    Dafür benögen wir noch eine extra Regeln:
    Falls das Epansionsmuster

    ( expression )
    

    ist, werden die Klammern bei der Expansion ignoriert.

    s << (args << ' ') <<...; // wird zu s << arg0 << ' ' << arg1 << ' ' usw.
    s << ((args << ' ')) <<...; // wird zu s << (arg0 << ' ') << (arg1 << ' ') usw.
    

    Außerdem fehlt mir noch ein lazy-Operator um vorzeitige Packexpansion zu verhindern. Ich nehme mal -> als Präfixoperator:

    template <int... i> index_list {
      using register int pack = i; // etwas sinnvollere Syntax für höhencollers Idee
    };
    
    template <int rows, int columns>
    struct matrix
    {
        double m[rows][colums];
    ...
    };
    
    template <int X, int Y, int Z>
    matrix<X, Z> operator*(matrix<X,Y> a, matrix<Y,Z>)
    {
        using register int x_list = make_indexes<X>::pack;
        using register int y_list = make_indexes<y>::pack;
        using register int z_list = make_indexes<z>::pack;
        return matrix<X, Z>{ a.m[->->x_list][y_list]*b.m[y_list][->z_list]+... ... ... };
    }
    


  • Da werden die Javaner richtig doof aus der Wäsche gucken.

    Zum Folgenden "Problem" bzw. der folgenden Unklarheit schlage ich vor, dass Ausdrücke mit einer Ellipsis als Operand stärker gebunden werden:

    camper schrieb:

    camper schrieb:

    Evtl. verfeinert man das noch mit ein Kollapsregel, wenn der gleiche Operator, der für die Packexpansion genutzt wird, unmittelbar zuvor oder danach auftritt:

    Was ist mit anderen Operatoren in der Nähe?

    auto serial_size = 0 + (1 * sizeof(T)) + ... // erlaubt?
    

    Klar, wird (sobald die 1 weg ist) zu (sizeof(T) + ...) , was wiederum ganz klar definiert ist

    auto serial_size = 0 + 1 * sizeof(T) + ... // illformed oder wie Zeile oben?
    

    Hier greift die Regel ganz normal: Es ist 1 * (sizeof(T) + ...) ...

    auto custom_sizeof = sizeof...(T)==0 ? 0 : sizeof(T) + ...; // ill-formed?
    

    Nö! Dank der stärkeren Bindung wird das wieder ok sein.

    std::cout << args << ... << '\n'; // vermutlich std::cout << (args << ...) << '\n' und std::cout << '\n'
    

    Ja, das wird dann nicht möglich sein.



  • camper schrieb:

    höhenkoller schrieb:

    std::cout << "{ " << (args << ' ' <<)... << "}\n"; // Geht sowas ohne Workaround?
    

    Dafür benögen wir noch eine extra Regeln:
    Falls das Epansionsmuster

    ( expression )
    

    ist, werden die Klammern bei der Expansion ignoriert.

    s << (args << ' ') <<...; // wird zu s << arg0 << ' ' << arg1 << ' ' usw.
    s << ((args << ' ')) <<...; // wird zu s << (arg0 << ' ') << (arg1 << ' ') usw.
    

    Oh, das ist schön! 😋 👍



  • camper schrieb:

    höhenkoller schrieb:

    camper schrieb:

    StackOps<ArgTypes>::push(L, args) , ...; // Kommaoperator
    

    Das finde ich sehr schön. Allerdings gibt man damit die Garantie auf, dass die Auswertung sequenced ist (der Kommaoperator kann überladen werden).

    Sehe ich hier nicht als Problem an, wer diesen Operator überlädt, muss sich über die Konsequenzen klar sein.

    Das Problem ist das, dass damit Klasseninvarianten gebrochen werden können. Du wirst das wohl auf Machiavelli verweisend akzeptieren, aber ich möchte meine Klassennutzung nicht willkürlich einschränken. Lieber nehme ich die alte Syntax mit dem Array. Vieleicht sollte für den Kommaoperator mit Pack die Auswertung der Argumente gesequenced sein.

    Für Expansionen mit leeren Packs könnte man einen neuen eingebauten Typ (meinetwegen: std::empty_pack_expression_t) einführen. Für die eingebauten Operatoren erklärt man die Semantik einfach so, dass jeweils der andere Operand das Ergebnis darstellt, und bei Überladung kann sich der Programmierer eigene Gedanklen machen.

    Das löst auch das ?:-Problem (@Sone: Hast du schon den Fall sizeof...(T)==0 angeschaut?). Der Unterschied ist allerdings detektierbar:

    decltype(variable + args + ...); // decltype(variable) hat spezielle Bedeutung
    
    template <int... i> index_list { 
      using register int pack = i; // etwas sinnvollere Syntax für höhencollers Idee 
    };
    

    Ich will die gleiche Syntax auch für Typlisten um perfect forwarding für Template-Parameter zu ermöglichen. Momentan habe ich für mich die Richtlinie, alle Werte als integral_constant zu übergeben.

    template <tempalte<register...>class T, register F, typename... Args>
    T<F...> template_parameter_forward(Args&&... args) { return T<typename F...>(std::forward<Args>(args)...); }
    
    // Um Variablen zu deklarieren (da müsste deine Definition ggf. erweitert werden):
    typename F var...;
    
    auto a = template_parameter_forward<vector, int>();
    auto b = template_parameter_forward<array, 5>();
    auto c = template_parameter_forward<matrix, make_indices<2>::pack>();
    auto d = template_parameter_forward<matrix, register<1, 2>>();
    auto e = template_parameter_forward<vector, register<int, std::allocator>>();
    

    Das kann man dann mit dem Int-Pack kombinieren:

    template <int... i> index_list { 
      using register pack = i; // allgemeinere Syntax
    };
    

    Gefällt mir echt 🙂

    typedef std::tuple<...> tuple; tuple t;
    return 0 + std::get<int_seq_pack<std::tuple_size<tuple>::value>(t) + ...;
    

  • Mod

    Syntax könnte auch so aussehen (wir wissen ja schließlich bereits, wie Packs zu deklarieren sind, kein Grund, etwas neues zu erfinden, die Syntax von typedef zu erweitern funktioniert aber wahrscheinlich nicht).

    int... int_pack = 1, 2, 3;
    int... int_pack2 = int_pack...;
    using... Args = int, double, char;
    Args... args = 0, 0.0, '\0';
    
    template <typename... T>
    using... pointer_added = T*...;
    
    using... template <typename> class funcs = std::plus, std::minus;
    
    template <typename... T>
    void foo(T&&... x)
    {
        using... Args = T&&...;
        Args... args = x...;
    }
    

    Der Schreibvorteil deines template_parameter_forward ist mir nicht so klar.



  • camper schrieb:

    Syntax könnte auch so aussehen (wir wissen ja schließlich bereits, wie Packs zu deklarieren sind, kein Grund, etwas neues zu erfinden, die Syntax von typedef zu erweitern funktioniert aber wahrscheinlich nicht).

    int... int_pack = 1, 2, 3;
    int... int_pack2 = int_pack...;
    using Args... = int, double, char;
    Args... args = 0, 0.0, '\0';
    
    template <typename... T>
    using pointer_added... = T*...;
    
    using template <typename> class funcs... = std::plus, std::minus;
    
    template <typename... T>
    void foo(T&&... x)
    {
        using Args... = T&&...;
        Args... args = x...;
    }
    

    👍

    Mir gefällt wie sich der Thread entwickelt. Wer schreibt das Proposal? 🤡


  • Mod

    höhenkoller schrieb:

    camper schrieb:

    höhenkoller schrieb:

    camper schrieb:

    StackOps<ArgTypes>::push(L, args) , ...; // Kommaoperator
    

    Das finde ich sehr schön. Allerdings gibt man damit die Garantie auf, dass die Auswertung sequenced ist (der Kommaoperator kann überladen werden).

    Sehe ich hier nicht als Problem an, wer diesen Operator überlädt, muss sich über die Konsequenzen klar sein.

    Das Problem ist das, dass damit Klasseninvarianten gebrochen werden können. Du wirst das wohl auf Machiavelli verweisend akzeptieren, aber ich möchte meine Klassennutzung nicht willkürlich einschränken. Lieber nehme ich die alte Syntax mit dem Array. Vieleicht sollte für den Kommaoperator mit Pack die Auswertung der Argumente gesequenced sein.

    Ich mag Ausnahmeregelungen nicht.
    Anfreunden könnte ich mich mit

    expression ...
    

    wird expandiert als Ausdruck, der die einzelnen Elemente mit dem eingebauten Kommaoperator verknüpft mit der Maßgabe, dass wenn die Grammatik an der Stelle auch

    expression-list
    

    zulässt (Funktionsaufrufe, Initialisierung), diese Interpretation vorrang hat. Wer das Andere will, kann ja einfach eine Klammer verwenden

    (expression ...)
    

  • Mod

    höhenkoller schrieb:

    Ich will die gleiche Syntax auch für Typlisten um perfect forwarding für Template-Parameter zu ermöglichen. Momentan habe ich für mich die Richtlinie, alle Werte als integral_constant zu übergeben.

    template <tempalte<register...>class T, register F, typename... Args>
    T<F...> template_parameter_forward(Args&&... args) { return T<typename F...>(std::forward<Args>(args)...); }
    
    // Um Variablen zu deklarieren (da müsste deine Definition ggf. erweitert werden):
    typename F var...;
    
    auto a = template_parameter_forward<vector, int>();
    auto b = template_parameter_forward<array, 5>();
    auto c = template_parameter_forward<matrix, make_indices<2>::pack>();
    auto d = template_parameter_forward<matrix, register<1, 2>>();
    auto e = template_parameter_forward<vector, register<int, std::allocator>>();
    

    Im Prinzip ist register F hier ja auch nichts anderes als int... F nur, dass es nicht expandierte Packs aufnimmt. Das könnte man auch implizit lösen, indem man die Bedingung, dass das Primärtemplate nur maximal ein Parameterpack haben kann, aufgibt (ein match ist dann entweder die übliche Liste von Argumenten oder aber ein nicht expandiertes Pack).

    template <tempalte<int...>class T, int... F, typename... Args>
    T<F...> template_parameter_forward(Args&&... args) { return T<typename F...>(std::forward<Args>(args)...); }
    
    int... var = F...;
    
    auto b = template_parameter_forward<array, <5> >(); // <list>  für in-place Deklaration eines Packs (müsste ggf. auf Mehrdeutigkeit untersucht werden).
    auto c = template_parameter_forward<matrix, make_indices<2>::pack >();
    auto d = template_parameter_forward<matrix, <1, 2> >();
    auto e = template_parameter_forward<vector, <int, std::allocator> >(); // das geht dann nat. nicht
    


  • camper schrieb:

    wir wissen ja schließlich bereits, wie Packs zu deklarieren sind, kein Grund, etwas neues zu erfinden

    Never mind. Ich dachte, einmal hätte ich mir einmal einen Template-Parametertyp gewünscht, der alle 4 Parametertypen matchen kann. Wenn ich mich recht erinnere, war das aber nur, um ein bind für Templateparameter zu schreiben um Template-Lambdas zu ermöglichen. Deine Vorschläge hier machen das aber grösstenteils überflüssig. Explizit den Typ angeben dürfte ausreichen.

    expression ... finde ich super, das war auch das, was ich vorgeschlagen hatte. Die std::empty_pack_expression_t gefällt ich immer besser, weil sonst hätte f(args + ...) unterschiedlich viele Parameter. Es darf aber auf keinen Fall eine implizite Konvertierung zu einem anderen Typen stattfinden.

    Wenn Args... args lokal erlaubt ist, sollte das auch als Rückgabetyp erlaubt sein, dann hätten wir ein echtes Built-In-Tupel (std::get muss noch überladen werden). Ach, ich wäre schon überglücklich, wenn auch nur ein Teil der Features übernommen würde.



  • höhenkoller schrieb:

    std::get muss noch überladen werden

    template <int N, typename... T>
    auto get(T&&... args) { return (make_indices<N>::pack, std::forward<T>(args))...; }
    

  • Mod

    höhenkoller schrieb:

    Wenn Args... args lokal erlaubt ist, sollte das auch als Rückgabetyp erlaubt sein, dann hätten wir ein echtes Built-In-Tupel (std::get muss noch überladen werden). Ach, ich wäre schon überglücklich, wenn auch nur ein Teil der Features übernommen würde.

    Habe mir überlegt, dass das (bezogen auf std::tuple) redundant ist, wenn man dieser Form nicht eine andere Initialisierungssemantik gibt.

    Evtl. könnte man in betracht ziehen, dass die initialisierenden Elemente erst bei der Expansion ausgewertet werden (wie ein Makro)

    template <typename... T>
    void foo(T&&... args)
    {
        T&... a = ++args...;
        cout << ( a << ' ' ) << ... << '\n';
        cout << ( a << ' ' ) << ... << '\n';
    }
    
    foo(1,2,3);
    

    gibt 2 mal 2 3 4 aus, wenn die Auswertung bereits bei der Definition von a erfolgt.
    Andernfalls

    2 3 4
    3 4 5
    

    Die erste Variante könnte man aber auch einfach mit einem std::tuple bekommen.



  • camper schrieb:

    Habe mir überlegt, dass das (bezogen auf std::tuple) redundant ist, wenn man dieser Form nicht eine andere Initialisierungssemantik gibt.

    Das hört sich an, als wolltest du die Semantik um der Redundanz willen ändern. Ich finde das Verhalten unintuitiv.
    Lazy kann übrigens auch durch eine Wrapperklasse erreichen, kein Grund, das in die Sprache aufzunehmen:

    lazy<T>... a = ++make_lazy(args)...;
    cout << ( a << ' ' ) << ... << '\n';
    cout << ( a << ' ' ) << ... << '\n';
    

    Eine nutzbare Syntax für std::tuple finde ich hingegen schon ein guter Grund. Es gibt dieses int_seq-Proposal, aber wirklich Spass macht Tupelprogrammierung damit immer noch nicht.


  • Mod

    höhenkoller schrieb:

    lazy<T>... a = ++make_lazy(args)...;
    cout << ( a << ' ' ) << ... << '\n';
    cout << ( a << ' ' ) << ... << '\n';
    

    Mit Expressiontemplates?

    höhenkoller schrieb:

    Es gibt dieses int_seq-Proposal, aber wirklich Spass macht Tupelprogrammierung damit immer noch nicht.

    N3493? Scheint nur standardisieren, was sowieso schon gemacht wird.

    template <typename... T>
    void foo(T&&... args)
    {
        auto a = make_tuple( ++args... );
        cout << ( std::get<a.indexes>(a) << ' ' ) << ... << '\n'; // tuple müsste entsprechend erweitert werden
    }
    

    sehe ich nicht als besonders umständlich an

    Ich bin ein bisschen unsicher, ob frühzeitige Auswertung ggf. ineffizient ist, gerade dann, wenn es für forwarding-Zwecke benutzt werden soll. Denn dann kommt die Implementation kaum darum herum, Diese Tuple wie echte Objekte zu implementieren (mit dem damit verbundenen Kopieraufwand).


  • Mod

    Die Idee mit lazy Auswertung ist wahrscheinlich keine Gute, oder hat jedenfalls nichts speziell mit Packs zu tun.

    Es gibt noch ein anderes Problem, das zu lösen wäre

    template <typename... T>
    struct foo
    {
        static T... data;
    };
    
    using... list = foo<int>, foo<char, float, double>;
    
    list::data + ...; // ???
    

    Wenn Packs auch ausserhalb von Template- und Funktionsargumenten auftreten können, können sie nat. auch Member einer Klasse sein. Und wenn diese Klasse selbst in einer Liste auftaucht, funktioniert die einfache Regel, dass alle nicht expandierten Packs gleichzeitig expandiert werden, offenbar nicht mehr. Zudem ergibt sich ein syntaktisches Problem innerhalb von Templates, der Compiler müsste wissen, dass es sich bei einem verschachtelten Bezeichner um ein Pack handelt, man braucht also die Möglichkeit, so diese zu kennzeichnen, so wie das bei Membertemplates der Fall ist.



  • camper schrieb:

    Mit Expressiontemplates?

    Ähnlich zu Boost.Lambda. auto&&... a = freeze(++make_lazy(args))...; ohne Deduzieren von Konstruktortemplateargumenten. Ist nicht 100% das gleiche (args + (std::cout << 'x')), dafür muss man dann einen Umweg über lazy_apply(args, [](auto x){...})... gehen. Hat ausserdem syntaktische Unterschiede, solange sich der Punkt-Operator nicht überladen lässt (da habe ich auch noch kein Proposal gesehen). Aber möglich ist es schon.

    funktioniert die einfache Regel, dass alle nicht expandierten Packs gleichzeitig expandiert werden, offenbar nicht mehr.

    Das ist doch gewollt, damit bekommen wir die Lazy-Expandierung:

    template <int X, int Y, int Z>
    matrix<X, Z> operator*(matrix<X,Y> a, matrix<Y,Z>)
    {
        int... ... ... x_list = ((make_indexes<X>::pack>));
        int... ...     y_list = (make_indexes<y>::pack);
        int...         z_list = make_indexes<z>::pack;
        return matrix<X, Z>{ a.m[x_list][y_list]*b.m[y_list][z_list]+... ... ... };
    }
    

    Die Regel ist dann, dass immer alle äusserste Packs expandieren.

    Wäre noch zu überlegen, was das für Auswirkungen hat, Templates ohne template<...> schreiben zu können.

    int... foo() { return <1, 2>; } // ??
    foo<char, float>::data bar(); // ??
    typedef decltype(list::data) typedefed_pack; // ??
    

    Ich tendiere dazu, das erste zu verbieten (Länge müsste Teil der Signatur sein), beim Rest bin ich noch unsicher.


  • Mod

    Vorschlag für das Problem:
    erlaube die Verschachtelung von Ellipsen (so wie mein lazy-Op-Vorschlag, der damit wahrscheinlich sogar überflüssig wird)

    template <int... i>
    struct foo
    {
        static constexpr int pack = i...;
    };
    
    using... foos = foo<0,1>, foo<2,3>;
    
    foos::pack ... ...
    

    Dann gibt es im Prinzip 2 Möglichkeit:
    Variante 1. expandiere alle Ellipsen, die nicht selbst Teil eines Expansionsmusters sind. Wiederhole bis alle Ellipsen expandiert wurden "von außen nach innen"
    1. Schritt

    foo<0,1>::pack ... ,  foo<2,3>::pack ...
    

    2. Schritt
    foo<0,1>::pack0, foo<0,1>::pack1, foo<2,3>::pack0, foo<2,3>::pack1
    also

    0, 1, 2, 3
    

    Variante 2. expandiere alle Ellipsen, die keine Ellipse in ihrem Expansionsmuster haben. Wiederhole bis alle Ellipsen expandiert wurden. "von innen nach aussen"
    1. Schritt

    ( foo<0,1>::pack ,  foo<2,3>::pack ) ...
    

    (die Klammern nur gedacht)
    2. Schritt
    foo<0,1>::pack0, foo<2,3>::pack0, foo<0,1>::pack1, foo<2,3>::pack1
    also

    0, 2, 1, 3
    

    Ich würde Variante 1 bevorzugen, aber evtl. übersehe ich noch etwas.

    Edit: da war ich ein bisschen langsam beim Schreiben, hatte den vorherigen Beitrag noch nicht gesehen...


  • Mod

    höhenkoller schrieb:

    template <int X, int Y, int Z>
    matrix<X, Z> operator*(matrix<X,Y> a, matrix<Y,Z>)
    {
        int... ... ... x_list = ((make_indexes<X>::pack>));
        int... ...     y_list = (make_indexes<y>::pack);
        int...         z_list = make_indexes<z>::pack;
        return matrix<X, Z>{ a.m[x_list][y_list]*b.m[y_list][z_list]+... ... ... };
    }
    

    Mit der Syntax kann ich mich nicht anfreunden. Ist auch nicht erforderlich, wenn wir ein Hilfstemplate einführen:

    template <typename... T>
    struct pack_identity
    {
        using... types = T...;
    };
    
    template <int X, int Y, int Z>
    matrix<X, Z> operator*(matrix<X,Y> a, matrix<Y,Z>)
    {
        using... x_list = pack_identity<make_indexes<X>>;
        int...   y_list = make_indexes<y>::pack...;
        using... z_list = make_indexes<z>;
        return matrix<X, Z>{ a.m[x_list::types::pack][y_list]*b.m[y_list][z_list::pack]+... ... ... };
    }
    

    höhenkoller schrieb:

    Wäre noch zu überlegen, was das für Auswirkungen hat, Templates ohne template<...> schreiben zu können.

    int... foo() { return <1, 2>; } // ??
    foo<char, float>::data bar(); // ??
    typedef decltype(list::data) typedefed_pack; // ??
    

    Ich tendiere dazu, das erste zu verbieten (Länge müsste Teil der Signatur sein), beim Rest bin ich noch unsicher.

    Das habe ich nicht auf der Agenda. ich betrache ... als Deklarator, der nur direkt top-level eingesetzt werden kann.

    int... foo();
    

    deklariert dann ein Pack aus FUnktionen, was ich verbieten würde. Ich sehe hier auch keinen Grund, nicht std::tuple einzusetzen.



  • Damit Variante 1 möglich ist, braucht es ein paar Zusatzregeln.

    using... a = int, char, long;
    using... b = float, double;
    using... c = pair<a, b>...;
    c == pair<int,float>, pair<char,double> // sonst inkonsistent
    
    int... a = 1, 2, 3;
    int... b = 4, 5;
    using... c = matrix<a, b>...;
    c == matrix<1, 4>, pair<2, 5> // sonst inkonsistent
    
    int... a = 1, 2, 3;
    int... b = 4, 5;
    int c = (a + b) *...;
    c == (1+4) * (2+5) // sonst inkonsistent
    
    int... a = foo<1,2>::pack;
    int... b = foo<3,4,5>::pack;
    using... c = matrix<a,b>... ...
    c == matrix<1,3>, matrix<2,4>
    
    int... a = foo<1,2>::pack;
    int... b = foo<3,4,5>::pack;
    using... ab = a, b; // Pseudo-Syntax
    int... c = identity<ab...>... // Eher 1,3,2,4
    int... c = (ab...) ... // Eher 1,3,2,4
    int... c = ab... ... // Kaum 1,2,3,4,5
    

Anmelden zum Antworten