Und mal wieder variadische Templates...



  • 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
    

  • Mod

    Wie diese Beispiele zu behandlen sind, legt bereits der jetzige Standard fest. Dein Vorschlag würde existierenden Code verändern.



  • Mein Vorschlag? Es ging mir darum, das Verhalten für

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

    abzuleiten, und das sieht halt stark nach

    0,2,1,3
    

    aus.


  • Mod

    Habe noch einmal etwas nachgedacht.

    int... foo()
    

    wäre nat. eine Funktion, die ein Pack zurückgibt. Ich würde so etwas entweder ganz verbieten, oder, wenn wie hier die Ellipse nicht auf ein Pack einwirken kann, dies so interpretieren, dass das Pack aus einem einzelnen Element bestehen soll.

    Bei Referenz-/Objektdefinitionen können wir einfach verlangen, dass stehts eine Initialisiererlist angegeben werden muss, aus der dann die Anzahl bestimmt werden kann.

    Die Möglichkeit Packs zurückzugeben, ist eigentlich weitgehend redundant. Mir fällt eigentlich nur ein nützlicher Fall ein, um damit auf eine spezielle Syntax zum Deklarieren von anonymen Packs verzichten zu können.

    template <typename... T>
    constexpr T&&... value_pack(T&&... args)
    {
        return... std::forward<T>(args)...; // oder ggf. mit einfachem return, falls hier die Syntax kontextabhängig modifiziert wird
    }
    
    template <typename... T>
    struct identity
    {
        using... types = T...;
    };
    template <typename T>
    struct identity
    {
        using type = T;
        using... types = T;
    };
    
    // pack aus Werten:
    value_pack(1,2,3)
    // pack aus Typen
    identity<char,int,double>::types
    
    using... a = int, char, long;
    using... b = float, double;
    using... c = pair<a, b>...;
    c == pair<int,float>, pair<char,double> // sonst inkonsistent
    

    ill-formed

    int... a = 1, 2, 3;
    int... b = 4, 5;
    using... c = matrix<a, b>...;
    c == matrix<1, 4>, pair<2, 5> // sonst inkonsistent
    

    ill-formed

    int... a = 1, 2, 3;
    int... b = 4, 5;
    int c = (a + b) *...;
    c == (1+4) * (2+5) // sonst inkonsistent
    

    ill-formed

    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>
    

    ill-formed (schon unter der ersten Ellipse)

    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
    

    ebenfalls ill-formed
    in keinen von deinen Beispielen tritt die Form pack::nested_pack auf.


Anmelden zum Antworten