C-Array an Konstruktor übergeben (rvalue reference?)



  • Wie willst du denn wie Werte für die einzelnen Elemente an den Konstruktor übergeben? Direkt als Liste von Konstanten? In dem Fall ginge es ohne Kopien, indem du ein Aggregate bastelst. Also eine Klasse ala std::array die keinen Konstruktor hat, dafür aber ein public Member welches das Array ist. Wie effizient der Code ist den dein Compiler daraus machen wird ist dann eine andere Frage, aber viel effizienter als das wird es wohl kaum gehen.

    Davon abgesehen: Irgendwie müssten die Bytes im RAM wo das Objekt konstruiert wird ja auf die gewünschten Werte gesetzt werden. Jetzt gibt es eigentlich nur mehr zwei Varianten:

    1. Die Werte im Array müssen nicht geändert werden. In dem Fall brauchst du gar keine Klasse, sondern solltest vermutlich einfach ein normales static const Array nehmen. Im Idealfall landen die Daten dann sogar in einem "read only" Bereich.
    2. Die Werte im Array müssen geändert werden können. In dem Fall musst du sie auf jeden Fall beim Erstellen eines Objekts initialisieren.
      2.1: Wenn du dafür einen einfachen Algorithmus verwenden kannst (wie z.B. "setze alles auf 0"), dann solltest du dafür einen speziellen Konstruktor programmieren. Der kann das dann möglichst effizient machen, ohne unnötige Umwege.
      2.2: Wenn du beliebige Werte brauchst, dann ist vermutlich der effizienteste Weg diese aus einem static const Array zu kopieren. Das ist das was du vermeiden willst, aber ... wie soll es sonst gehen? Unnötige Kopie gibt es dabei aber keine, es muss und wird genau 1x kopiert.


  • Ich brauche eine Lösung für mehrere Anwendungsfälle. In keinem davon ist das array static, in manchen aber const.

    Die Idee mit einer eigenen Array Klasse finde ich gut. Den entscheiden Hinweis hast du gegeben mit "public members" ... das man hier das Problem mit dem Konstruktor übergeht ist mir dadurch aufgefallen. Hier meine Umsetzung

    #ifndef ARRAY_H
    #define ARRAY_H
    
    #include <inttypes.h>
    
    //forward decleration
    template <typename T, uint8_t N>
    class Array;
    
    //use the name Array2D for an array with two dimensions
    template <typename T, uint8_t rows, uint8_t columns>
    using Array2D = Array<Array<T, columns>, rows>;
    
    template <typename T, uint8_t N>
    class Array {
    
    public:
      constexpr T& operator[](uint8_t position) const {
        return this->array[position];
      }
    
      constexpr uint8_t size() const {
        return N;
      }
    
      T data[N];
    };
    #endif //ARRAY_H
    

    Weitere nützliche Methoden wie ein geprüfter Zugriff über eine at Methode können natürlich noch hinzugefügt werden. Habt ihr hierzu noch Verbesserungsvorschläge?

    Meine Test Klasse sieht jetzt wie folgt aus:

    template <typename T, uint8_t N>
    class Test {
    
    public:
      Matrix(Array<T, N>&& data)
      : data {data};
    
      template <typename... U>
      Matrix(U... data)
      : data {data...} {}
    
      constexpr uint8_t size() const {
        return this->data.size();
      }
    
    private:
      Array<T, N> data;
    };
    

    Die Erstellung kann auf 2 Wegen erfolgen (je nachdem was die Klasse sonst noch so erfüllt, soltte nur der eine Konstruktor implementiert werden)

    Test<uint8_t, 2> test1 {{1,1}};
    Test<uint8_t, 2> test2 {1, 1};
    

    Erstere Methode soll dafür sein, wenn man noch weitere Parameter übergeben möchte (also das array auch wirklich ein Array sein soll), die zweite Methode z.B. für einen mathematischen Vektor usw., wo das Array nur intern bernutzt wird.

    Einziges Problem was ic habe ist mit dem zweiten Konstruktor. Ich möchte ja eine ellipse haben, aber mit dem Datentyp T. Sprich so:

      Matrix(T... data)
      : data {data...} {}
    

    um diese Warnung zu vermeiden:
    warning: narrowing conversion of ‘data#1’ from ‘int’ to ‘unsigned char’ inside { }

    Ich bin aber etwas ratlos wie ich das hinbekomme. Habt ihr diesbezüglich noch Ideen oder weitere Verbesserungsvorschläge?



  • Gegenfrage: was soll passieren, wenn du da 256 (oder mehr) reinschreibst? Die geschweiften Klammern soll ja gerade davor schützen, außerhalb des Wertebereichs zu kommen.
    Test<uint8_t, 2> test2 {(uint8_t)1, (uint8_t)1}; willst du dan nicht, nehme ich an?



  • Wenn man Werte > 255 reinschreibt in die geschweiften Klammern, sollte im besten Fall ein Fehler beim Kompilieren passieren oder zumindest eine Warnung kommen.
    Grade da aber durch den zusätzlichen Template Parameter U, der Typ automatisch bestimmt wird (hier int) wird das vermutlich nicht passieren. Deshalb würde ich mir wünschen, dass U vom selben Typ wie T ist.
    Exlplizit den Typ angegeben bei jeder Zahl würde gehen, wäre aber sehr unschön.


  • Mod

    @Leon0402 sagte in C-Array an Konstruktor übergeben (rvalue reference?):

    Einziges Problem was ic habe ist mit dem zweiten Konstruktor. Ich möchte ja eine ellipse haben, aber mit dem Datentyp T. Sprich so:

      Matrix(T... data)
      : data {data...} {}
    

    um diese Warnung zu vermeiden:
    warning: narrowing conversion of ‘data#1’ from ‘int’ to ‘unsigned char’ inside { }

    Ich bin aber etwas ratlos wie ich das hinbekomme. Habt ihr diesbezüglich noch Ideen oder weitere Verbesserungsvorschläge?

    Um ein Parameterpack (T...) expandieren zu können muss erst einmal ein Parameterpack existieren. Das kann nun kein Parameterpack des Konstruktors sein, weil, wie schon oben erwähnt, dann keine Konvertierungen für die
    Funktionsparameter stattfinden kann. Also muss das Pack woanders herkommen, nämlich dem Klassentemplate selbst. Die Anzahl der benötigten Funktionsparameter ergibt sich aus dem Templateargument N also müssen wir hieraus ein Pack konstruieren, analog zu make_index_sequence der Standardbibliothek. z.B. so:

    #include <inttypes.h>
    
    //forward decleration
    template <typename T, uint8_t N>
    class Array;
    
    //use the name Array2D for an array with two dimensions
    template <typename T, uint8_t rows, uint8_t columns>
    using Array2D = Array<Array<T, columns>, rows>;
    
    template <typename T, uint8_t N>
    class Array {
    
    public:
      constexpr T& operator[](uint8_t position) const {
        return this->array[position];
      }
    
      constexpr uint8_t size() const {
        return N;
      }
    
      T data[N];
    };
    
    template <int... I>
    struct index_sequence {
      using type = index_sequence;
    };
    
    template <int N, typename = index_sequence<>>
    struct make_index_sequence_impl;
    
    template <int N, int... I>
    struct make_index_sequence_impl<N, index_sequence<I...>>
    : make_index_sequence_impl<N-1, index_sequence<0, 1+I...>> {};
    
    template <int... I>
    struct make_index_sequence_impl<0, index_sequence<I...>>
    : index_sequence<I...> {};
    
    template <int N>
    using make_index_sequence = typename make_index_sequence_impl<N>::type;
    
    template <typename T, int I>
    using Ntype = T;
    
    template <typename T, int N, typename = make_index_sequence<N>>
    class TestImpl;
    
    template <typename T, int N, int... I>
    struct TestImpl<T, N, index_sequence<I...>> {
      TestImpl(const Array<T, N>& data)
      : data{data} {}
    
      TestImpl(Ntype<T, I>... v)
      : data{v...} {}
    
      Array<T, N> data;
    };
    
    template <typename T, uint8_t N>
    class Test : private TestImpl<T, N> {
    public:
      using TestImpl<T, N>::TestImpl;
    
      constexpr uint8_t size() const {
        return this->data.size();
      }
    };
    
    int main() {
      Test<uint8_t, 2> test1 {{1,1}};
      Test<uint8_t, 2> test2 {1, 1};
      // Test<uint8_t, 2> test3 {256, 1}; //  error: narrowing...
    }
    

    Die Trennung zwischen Test und TestImpl ist nicht erforderlich, hat aber den Vorteil, dass somit ggf. später auftretende Fehlermeldungen im Zusammenhang mit der Verwendung von Test nicht furchtbar unleserlich werden, weil ständig ein expandiertes Parameterpack in die Fehlermeldung integriert ist.



  • Hallo Camper,

    vielen Dank für deine Hilfe! Leider kann ich deinem Code nicht folgen, meine Kenntnisse über Templates sind bescheiden.
    Wäre es dir möglich, mir den Code etwas näher zu bringen?

    Um etwas mehr Anhaltspunkte zu geben:

    Deine Erklärungen leuchten mir soweit ein, dass ein Parameter Pack konstruiert werden muss und dieses die Größe des Templatearguments N hat. Unbekannt dagegen ist mir make_index_sequence aus der Standardbibliothek und einige Konstrukte, die du verwendet hast.

    template <int... I>
    struct index_sequence {
      using type = index_sequence;
    };
    

    Hier sehe ich zum ersten mal int... I im template, bekannt war mit nur template... I
    Eine google Suche hat ergeben, das es sich hierbei um non-type variadic templates handelt. Was genau das ist und vor allem wie man es anwendet, konnte ich allerdings nicht finden. Handelt es sich einfach um ein parameter Pack vom Typ int?
    Wenn ja, kann man dann nicht auch ein parameter Pack vom Typ T haben?

    template <typename T> 
    struct Test {
      template <T... I> 
      doSomething(I... args);
    }
    

    Desweiteren verwirt mich hier auch die Zeile

    using type = index_sequence;
    

    type wird hier doch nur zu einem alias von index_sequence ... Was bringt das? Ist der Alias nicht auf den scope von dem struct beschränkt? Und verwendet wird es ja auch nicht ...

    Auch dieses Konstrukt kenne ich nicht

    typename = index_sequence<>
    

    Handelt es sich dabei um einen default template value? Würde da nicht ein Name fehlen

    typename T = index_sequence<>
    

    Vielen Dank für deine Hilfe! Ich hoffe, wenn du oder jemand anderes mir diese Konstrukte erklärt, mir der source Code klarer wird.
    Im allgemeinen kommt der source code mir sehr komplex und lang vor für eine eigentlich so geringe Anfoderung🧐 Geht das wirklich nicht einfacher?


  • Mod

    @Leon0402 sagte in C-Array an Konstruktor übergeben (rvalue reference?):

    Deine Erklärungen leuchten mir soweit ein, dass ein Parameter Pack konstruiert werden muss und dieses die Größe des Templatearguments N hat. Unbekannt dagegen ist mir make_index_sequence aus der Standardbibliothek und einige Konstrukte, die du verwendet hast.

    template <int... I>
    struct index_sequence {
      using type = index_sequence;
    };
    

    Hier sehe ich zum ersten mal int... I im template, bekannt war mit nur template... I
    Eine google Suche hat ergeben, das es sich hierbei um non-type variadic templates handelt. Was genau das ist und vor allem wie man es anwendet, konnte ich allerdings nicht finden. Handelt es sich einfach um ein parameter Pack vom Typ int?

    So ist es: ein Template kann ja ganz allgemein type Templateparameter (1), non-type Templateparameter oder Template-Templateparameter haben:

    template <typename x, int y, template <typename> class z> class foo;
    

    Jedes dieser Parameterarten kann Parameterpacks bilden, in denen jedes einzelne Element des Packs von der entsprechenden Parameterart ist.

    Wenn ja, kann man dann nicht auch ein parameter Pack vom Typ T haben?

    template <typename T> 
    struct Test {
      template <T... I> 
      doSomething(I... args);
    }
    

    Parameterpacks vom Typ T sind an sich kein Problem, die einzelnen Elemente des Packs sind dann aber offenbar keine Typen und können folglich (ohne extra Transformation) nicht dazu dienen, die Parametertypen einer Funktion zu spezifizieren.

    Desweiteren verwirt mich hier auch die Zeile

    using type = index_sequence;
    

    type wird hier doch nur zu einem alias von index_sequence ... Was bringt das? Ist der Alias nicht auf den scope von dem struct beschränkt? Und verwendet wird es ja auch nicht ...

    Verwendet wird es hier:

    template <int N>
    using make_index_sequence = typename make_index_sequence_impl<N>::type;
    

    Das dient der Bequemlichkeit, auf diese Weise ist index_sequence<...> selbst eine Metafunktion ohne Parameter, die auf sich selbst abbildet.
    Jedes make_index_sequence<N> erbt ja ultimativ von der entsprechenden Version von index_sequence, und somit wird auch type vererbt; das macht es möglich, den gesamten rekursiven Aufruf direkt durch Vererbung darzustellen, was etwas kompakterem Code ermöglicht. In der Literatur oder Tutorials geht man meist den Weg, in Metafunktionen die entsprechenden typedefs jedesmal explizit vorzunehmen.
    Ich sollte erwähnen, dass der gezeigte Code nicht versucht effizient zu sein: die rekursive Instantiierungstiefe ist hier linear zu N. Das kann man besser machen und überlasse ich zur Übung.

    Auch dieses Konstrukt kenne ich nicht

    typename = index_sequence<>
    

    Handelt es sich dabei um einen default template value? Würde da nicht ein Name fehlen

    typename T = index_sequence<>
    

    Parameternamen kannst du in Templatedeklarationen ebenso wie in Funktionsdeklarationen weglassen, wenn der Name in der Definition nicht weiter gebraucht wird. Eine Defaultargument kann trotzdem angegeben werden.

    Wenn Templates nur für diesen isolierten Fall gebraucht werden, könnte man auch auf index_sequence<int...> verzichten, und direkt eine Typliste types<typename...> konstruieren. Das erspart dann den Ntype-Trick (mit Standardbibliotekt hätte ich hier direkt std::select eingesetzt).



  • So um alles zu verstehen:

    template<int... I>
    doSomething();
    

    ist quasi eine Ansammlung an non-type template Parametern(wobei nicht festgelegt ist wie viele)

    template<int a, int b, int c> 
    doSomething(); 
    
    doSomething<1,2,3>()
    

    wäre das ganze ausgeschrieben, nur das die Parameteranzahl nicht auf 3 begrenzt ist. Aus diesem Grund funktioniert mein Code mit T... auch nicht so wie ich mir das vorstelle, da daraus quasi sowas entstehen würde:

    doSomething(1 param1, 2 param2, 3 param3); 
    

    was natürlich unnsinn ist, weil ich will ja ein type Parameter haben (als parameter Pack), nur das der type nicht frei wählbar ist, sondern festgelegt auf einen Datentyp, nämlich T aus dem class Template.

    Deswegen muss ich mir ein eigenes Parameter Pack konzipieren, was den Datentyp T hat. Die Struktur des ganzen sieht so aus:

    template <int... I>
    struct a {};
    
    template <int... I>
    struct b : a<1 + I...> {};
    
    template <int... I>
    struct c : a<1 + I...> {};
    
    template <int... I>
    struct d : b<1 + I...> {};
    

    In deiner Fassung natürlich umgesetzt mit Templates ... so dass das die Tiefe n hat (Abbruchbedingung n = 0 -> template Spezialisierung).
    Wenn ich das richtig sehe ist hier eine Vererbungshirachie, wobei die oberste Klasse die komplette Liste an Integern hat, die in dem Array gespeichert werden sollen später (z.B. 1,2,3,4). Die Oberklasse von der, hat widerum eine Liste mit 1,2,3 als (non-type) template parameter. Deren Oberklasse eine Liste mit 1,2 ... die oberste Klasse dann nur noch die 1.
    Zumindests, wenn ich folgendes:

    <1 + I...>
    

    richtig deute?

    Der Datentyp NType kapselt dann den Datentyp T und diese Liste?

    template <typename T, int I>
    using Ntype = T;
    

    Offen bleiben tun natürlich trotzdem noch ein paar Sachen, die mir unklar sind (falls überhaupt das bisher Gesagte irgendwie stimmt😅 )

    1. Diese template struct deklarierst du vorher immer, wenn ich das richtige sehe. Sind das darauf folgende partielle Spezialisierungen?
    template <int N, typename I = index_sequence<>>
    struct make_index_sequence_impl;
    
    template <int N, int... I>
    struct make_index_sequence_impl<N, index_sequence<I...>>
    : make_index_sequence_impl<N-1, index_sequence<0, 1+I...>> {};
    

    Wenn es partiell wäre, müsste doch das int...I beim zweiten weg und dann nur unten in den spitzen Klammern angegeben werden, was da stattdessen eingesetzt wird (wie du es auch gemacht hast)?
    Oder ist das eine einfache Implementierung? (also vorher forward decleration) ... dann würde mir die Frage aufkommen, warum es plötzlich int.. I beim zweiten ist und nicht wie in der deklaration typename I.

    1. Was hat es mit dieser Zeile auf sich:
    template <int N>
    using make_index_sequence = typename make_index_sequence_impl<N>::index_sequence;
    

    also klar ist, dass es ein alias ist, scheinbar für die oberste Klasse ... weil ganz oben in der Vererbungshirachie steht ja immer die index_sequence. Aber was hat es mit dem typename davor auf sich? Das habe ich so (nicht in spitzen Klammern) noch nicht gesehen

    1. Inwieweit wird dieses T verwendet in
      template <typename T, int I>
      using Ntype = T;

    eig dachte ich, dass die non-type parameter Packs vom Typ T sein müssen.
    Sprich diese integer Sequenzen (int...) müssten T Sequenzen sein (T...).

    1. Natürlich dann noch wie die ganze Magie letzendes passiert ... dass man mit NType<T, I> ... die gewünschte Liste an uint8_t Parametern bekommt. Das ist aber vermutlich auch abhängig von dem restlichen Verständnis ... ob das soweit passt (Punkt 2... da das ja auch bei der Klasse Test gemacht wurde etc.).

    Vielen Dank für deine Hilfe! Entschuldige bitte meine vielen Fragen. Das bisherige übersteigt komplett meine template Kenntnisse, aber ich bin gewillt, da was nachzuholen 😉


  • Mod

    @Leon0402 sagte in C-Array an Konstruktor übergeben (rvalue reference?):

    Zumindests, wenn ich folgendes:

    <1 + I...>
    

    richtig deute?

    Da waren ein paar offensichtliche Schreibfehler drin, im Original füge ich ja am Anfang immer 0 hinzu, um eine Sequenz 0, 1, ..., N-1 zu erhalten.
    Eine andere Möglichkeit wäre, hinten anzufügen: <I..., sizeof...(I)>

    Der Datentyp NType kapselt dann den Datentyp T und diese Liste?

    template <typename T, int I>
    using Ntype = T;
    

    Ntype ist eine Templatemetafunktion die für jeden Index I den gewünschten Typ an dieser Stelle (nämlich T) liefert. Diese Funktion ist nun etwas langweilig, weil in diesem speziellen Fall das Ergebnis gar nicht vom konkreten Index abhängt.

    Hier noch das Ganze mit Standardmitteln (die betreffenden Header benötigen im Normalfall keine Runtimeunterstützung und stehen ja eventuell doch zur Verfügung).

    #include <type_traits>
    #include <utility>
    
    template <typename T, int N, typename = std::make_index_sequence<N>>
    class TestImpl;
    template <typename T, int N, std::size_t... I>
    struct TestImpl<T, N, std::index_sequence<I...>> {
      TestImpl(const Array<T, N>& data)
      : data{data} {}
    
      TestImpl(std::conditional_t<I, T, T>... v)
      : data{v...} {}
    
      Array<T, N> data;
    };
    

    Weil es sich um Standardmittel handelt, ist zu erwarten, dass nur minimal kommentiert werden muss, aus diesem Grunde ist mein Vorschlag, die entsprechende Funktionalität - sofern sie nicht direkt genutzt werden kann - nachzuprogrammieren.
    Wenn man den Umweg über index_sequence nicht gehen will, kann man die Typliste auch direkt konstruieren (hier mal als Beispiel ohne Verwendung von Verebung):

    template <typename... I>
    struct type_sequence {};
    
    template <int N, typename T, typename... L>
    struct make_type_sequence {
      using type = typename make_type_sequence<N-1, T, L..., T>::type;
    };
    
    template <typename T, typename... T>
    struct make_type_sequence<0, T, L...> {
      using type = type_sequence<L...>;
    };
    
    template <typename T, int N, typename = typename make_type_sequence<N, T>::type>
    class TestImpl;
    
    template <typename T, int N, typename... L>
    struct TestImpl<T, N, type_sequence<L...>> {
      TestImpl(const Array<T, N>& data)
      : data{data} {}
    
      TestImpl(L... v)
      : data{v...} {}
    
      Array<T, N> data;
    };
    

    Die Komplexität ist aber nicht erheblich geringer.

    1. Diese template struct deklarierst du vorher immer, wenn ich das richtige sehe. Sind das darauf folgende partielle Spezialisierungen?
    template <int N, typename I = index_sequence<>>
    struct make_index_sequence_impl;
    
    template <int N, int... I>
    struct make_index_sequence_impl<N, index_sequence<I...>>
    : make_index_sequence_impl<N-1, index_sequence<0, 1+I...>> {};
    

    Wenn es partiell wäre, müsste doch das int...I beim zweiten weg und dann nur unten in den spitzen Klammern angegeben werden, was da stattdessen eingesetzt wird (wie du es auch gemacht hast)?
    Oder ist das eine einfache Implementierung? (also vorher forward decleration) ... dann würde mir die Frage aufkommen, warum es plötzlich int.. I beim zweiten ist und nicht wie in der deklaration typename I.

    Das sind partielle Spezialisierungen, beim jetzigen Lesen fällt mir auf, dass ich mir das Verpacken in index_sequence in diesem Fall hätte sparen und direkt das Primärtemplate verwenden können.
    Ich verstehe die Frage nicht ganz, meinst du:

    template <int N, typename T>
    struct make_index_sequence_impl<N, index_sequence<I...>>
    ...
    

    ? Hier wird dich dar Compiler fragen, was I sein soll und ggf. darauf hinweisen, dass die partielle Spezialisierung nicht nutzbar ist, weil T nicht aus den konkreten Templateargumenten deduzierbar ist.

    Aber was hat es mit dem typename davor auf sich? Das habe ich so (nicht in spitzen Klammern) noch nicht gesehen

    typename ist erforderlich, wenn ein qualifizierter Bezeicher foo::bar innerhalb eines Templates verwendet wird und ein Typ sein soll, foo selbst von den Templateparametern des Templates abhängt, und wir uns nicht in bestimmten Kontexten befinden, in denen nur Typen auftreten können (in Basisklassenlisten ist kein typename erforderlich).
    Hier geht es darum, dass der Compiler schon viele Fehler in Templates finden soll, ohne diese instantiieren zu müssen.
    Betrachte:

    foo * bar;
    

    Das könnte eine Multiplikation sein, dann wenn foo und bar Variablen sind. Es könnte aber auch eine Deklaration eines Zeigers sein, wenn foo ein Typ ist. Ist nun foo ein abhängiger Bezeichner, kann der Compiler die Antwort nicht geben, solange die Templateparameter nicht bekannt sind; es wird unterstellt, dass foo kein Typ ist, wenn kein typename verwendet wird. Stellt sich dann später beim Instantiieren heraus, dass doch ein Typ gemeint war, ist das Programm fehlerhaft (wie auch im umgekehrten Fall).

    Ich empfehle für allgemeine Kenntnise zu Templateprogrammierung die einschlägige Literatur. Das Thema ist ein bisschen zu umfangreich, um es in Forenbeiträgen abzuhandeln.



  • So ich habe über die Implementation gegrübelt und denke das ich jetzt die Funktionsweise von der Integer Liste verstanden habe. Daher erstmal vielen Dank für deine hilfreiche Erklärungen, die mir dabei sehr geholfen hat.
    Meine Implementation (mit uint8_t statt size_t ... da das auf dem atmega oft der standard typ ist) sieht folgendermaßen aus:

    #ifndef INTEGERSEQUENCE_H
    #define INTEGERSEQUENCE_H
    
    //Sequence of uin8_t integers
    template <uint8_t ... Ints >
    class IndexSequence {};
    
    //Creates an IndexSequence through Inheritance -> O(n)
    template <uint8_t N, uint8_t... Ints>
    struct makeIndexSequenceImpl: public makeIndexSequenceImpl<N-1, N, Ints...> {};
    
    //Template specialization, termination condition of inheritance: n = 0
    template <uint8_t... Ints>
    struct makeIndexSequenceImpl<0, Ints...> : public IndexSequence<0, Ints...> {};
    
    //helper alias for the creation of an IndexSequence with O(n)
    template <uint8_t N>
    using makeIndexSequence = typename makeIndexSequenceImpl<N-1>::IndexSequence;
    
    /*
    //Sequence of integers with the type T
    template <typename T, T... Ints >
    class IntegerSequence {};
    
    //helper alias for the common case where T is uint8_t
    template<uint8_t... Ints>
    using IndexSequence = IntegerSequence<uint8_t, Ints...>;
    
    //Creates an IntegerSequence through Inheritance -> O(n)
    template <typename T, T N, T... Ints>
    struct makeIntegerSequenceImpl: public makeIntegerSequenceImpl<T, N-1, N, Ints...> {};
    
    //Template specialization, termination condition of inheritance: n = 0
    template <typename T, T... Ints>
    struct makeIntegerSequenceImpl<T, 0, Ints...> : public IntegerSequence<T, 0, Ints...> {};
    
    //helper alias for the creation of an IntegerSequence with O(n)
    template <typename T, T N>
    using makeIntegerSequence = typename makeIntegerSequenceImpl<T, N>::IntegerSequence;
    
    //helper alias for the creation of an IntegerSequence with O(n) for the common case where T is uint8_t
    template <uint8_t N>
    using makeIndexSequence = makeIntegerSequence<uint8_t, N>;*/
    
    #endif //INTEGERSEQUENCE_H
    

    Auskommentiert unten zu sehen ist ein Versuch neben einer IndexSequence eine allgemeinere IntegerSequence zu implementieren (wie es auch in der std der Fall ist). Das hat aber nicht funktioniert wegen der Template Spezialiserung von makeInetegerSequenceImpl, da N vom Typ T ist und daher nicht partiell spezialisiert werden kann. Ich glaube da gibt es ein paar workarounds🤔

    Unklar dagegen ist mir noch die Anwendung des ganzen für meinen Fall:

    #include "IntegerSequence.hpp"
    #include <iostream>
    
    template <typename T, uint8_t I>
    using ParamPack = T;
    
    template <typename T, uint8_t N, typename I = makeIndexSequence<N>>
    class Test;
    
    template <typename T, uint8_t N, uint8_t... I>
    class Test<T, N, IndexSequence<I...>> {
    
    public:
      void print(ParamPack<T, I>... v) {
        for(auto i :  {v...}) {
          std::cout << (uint16_t) i << " ";
        }
        std::cout << '\n';
      }
    };
    
    int main() {
      Test<uint8_t, 3> test {};
      test.print(1, 2, 3);
    }
    

    Hier hatte ich ja das Problem, dass mir diese Form der partiellen Spezialisierung nicht geläufig war erstmal. Meiner Vermutung nach ist das quasi eine partielle partielle Spezialisierung, wenn man das so sagen kann? typename I wird nicht komplett angegeben, aber teilweise (IndexSequence<I...>)

    template <typename T, uint8_t N, typename I = makeIndexSequence<N>>
    class Test;
    

    Die basis implementierung. Mit makeIndexSequence<N> als default Parameter (damit er bei der Erzeugung des Test Objekts nicht angegeben werden muss). Daraus entsteht quasi der Typ (bei Aufruf runten mit N=3) IndexSequence<0, 1, 2>.

    template <typename T, uint8_t N, uint8_t... I>
    class Test<T, N, IndexSequence<I...>> {
    

    Hier diese partielle Spezialiserung für IndexSequence. Ich schätze mal daraus kann der Compiler dann automatisch das non-type parameter pack I bestimmen (0,1,2).

    Für jeden Wert dieses paremeterPack soll dann in der Methode print ein Parameter vom Typ T bereitgestellt werden. Dazu bedient man sich diesem NType (bzw. hier ParamPack) Trick.

    void print(ParamPack<T, I>... v) 
    

    Dessen Funktionsweise ist eigentlich das einzig unklare noch. In Worten ausgedrückt, guckt er für jeden Index aus I nach, welchen Datentyp er nehmen soll. In dem Beispiel immer T, da ParamPack = T ja definiert wurde. Hier hattest du ja vorgeschlagen, da es immer T ist, den Trick sich zu sparen (wie das geht, habe ich nicht verstanden). Desweiteren wundert es mich, dass man einfach

    ParamPack<T, I> 
    

    schreiben kann, da I ja ein Parameter pack ist, das Param Pack dagegen erwartet ja nur ein int. Muss man I nicht irgendwie entpacken oder so? ... oder geschieht das automatisch, da ParamPack<T, I> ja als function Param Pack definiert ist?

    Du hattest auf Lektüre verwiesen. Du scheinst mir sehr viel Ahnung von templates zu haben, welche Lektüre würdest du empfehlen? Bzw. wie hast du es gelernt?
    Gutes gehört habe ich über:
    https://www.amazon.de/Templates-Complete-Guide-David-Vandevoorde/dp/0321714121/ref=pd_lpo_sbs_14_t_0?_encoding=UTF8&psc=1&refRID=HD7TWGQFSWXJ2MPGREN9

    Hier hätte ich auch (kostenlosen) Zugriff auf die ältere Version. Allerdings ist das schon sehr alt, darum frage ich mich, ob die neuere Version nicht lohnenswert wäre. Oder eben ein ganz anderes Buch.


  • Mod

    @Leon0402 sagte in C-Array an Konstruktor übergeben (rvalue reference?):

    Auskommentiert unten zu sehen ist ein Versuch neben einer IndexSequence eine allgemeinere IntegerSequence zu implementieren (wie es auch in der std der Fall ist). Das hat aber nicht funktioniert wegen der Template Spezialiserung von makeInetegerSequenceImpl, da N vom Typ T ist und daher nicht partiell spezialisiert werden kann. Ich glaube da gibt es ein paar workarounds🤔

    In diesem speziellen Fall dürfte es am einfachsten sein, einen bestimmten hinreichend großen (int dürfte schon reichen, für zu lange Argumentlisten wird der Compiler sowieso eher wegen Speichermangel aussteigen) Datentyp zu nehmen, damit die Sequenz zu konstruieren und am Anschluss nur die einzelnen Zahlen in einem Schritt nach T zu konvertieren. Ein Bedarf, für T(0) spezialisieren zu müssen, besteht dann nicht.

    Partielle Spezialisierungen werden häufig auf die Weise eingeführt, dass man eine 1:1 Korrespondenz zwischen Parametern der partiellen Spezialisierung und denen des Primärtemplates hat, wobei überzählige Parameter des Primärtemplates konstant sind (z.B. <T> -> <T*> oder <int, int> -> <int, 0>), das sind aber tatsächlich nur spezielle Fälle. Allgemein kann eine partielle Spezialisierung beliebige Templateparameter haben: was erforderlich ist, ist dass die Templateparameter der partiellen Spezialisierung aus der Argumentliste der Spezialisierung deduzierbar sind. Und wenn du Templateargumentdeduktion eher mit Templatefunktionen in Verbindung bringst, liegst du gar nicht so falsch: der Standard selbst erklärt den Mechanismus, wie partielle Spezialsiierungen ausgewählt werden, wenn eine konkrete Parameterliste vorliegt, im Prinzip dadurch, dass die partiellen Spezialisierungen umgeschrieben werden, als ob es sich um Templatefunktionen handelte, und dann werden die dafür bekannten Mechnismen (Argumentdeduktion + partielle Ordnung) darauf losgelassen. Man muss also im Grund nichts Neues lernen, wenn man Templatefunktionen verstanden hat.

      void print(ParamPack<T, I>... v) {
        for(auto i :  {v...}) {
          std::cout << (uint16_t) i << " ";
        }
        std::cout << '\n';
      }
    

    Die Ausgabe kann man optional auch ohne Schleife schreiben. Vor C++17 nur mit Tricks:

    void print(ParamPack<T, I>... v) {
      bool dummy[] = { std::cout << (uint16_t)v << ' ' ..., std::cout << '\n' };
    }
    

    Mit C++17 dann auf vernünftigere Weise per Fold-Ausdruck:

    void print(ParamPack<T, I>... v) {
        ( std::cout << (uint16_t)v << ' ', ... ) << '\n';
    }
    

    Desweiteren wundert es mich, dass man einfach

    ParamPack<T, I> 
    

    schreiben kann, da I ja ein Parameter pack ist, das Param Pack dagegen erwartet ja nur ein int. Muss man I nicht irgendwie entpacken oder so?

    Wenn du ein Pack hast, muss es irgendwann expandiert werden (wenn es nicht gerade Operand des sizeof.... Operators ist), aber nicht unbedingt sofort. Der Expansionsoperator ... operiert ja nicht einfach nur auf Parameterpacks sondern auf Expansionsmustern:
    mit I... -> 0, 1, 2 :
    ParamPack<T, I...> wird zu ParamPack<T, 0, 1, 2>
    ParmPack<T, I>... wird zu ParamPack<T, 0>, ParamPack<T, 1>, ParamPack<T, 2>

    Heutzutage lese ich eigentlich nur noch im Standard. Mit aktueller Literatur kann ich daher eher nicht selbst dienen. Stroustrup ist allerdings immer empfehlenswert.



  • @camper sagte in C-Array an Konstruktor übergeben (rvalue reference?):

    In diesem speziellen Fall dürfte es am einfachsten sein, einen bestimmten hinreichend großen (int dürfte schon reichen, für zu lange Argumentlisten wird der Compiler sowieso eher wegen Speichermangel aussteigen) Datentyp zu nehmen, damit die Sequenz zu konstruieren und am Anschluss nur die einzelnen Zahlen in einem Schritt nach T zu konvertieren. Ein Bedarf, für T(0) spezialisieren zu müssen, besteht dann nicht.

    Das wäre vermutlich die einfachste Lösung, die ich auch erstmal verwendet hatte. Ich dachte es geht vlt. mit irgenwas mir unbekannten besser, um es perfekt zu machen. Die Tricks um das zu ermöglichen, sind es aber bisher nicht wert gewesen, daher werde ich wohl bei einem int bleiben.

    @camper sagte in C-Array an Konstruktor übergeben (rvalue reference?):

    Mit C++17 dann auf vernünftigere Weise per Fold-Ausdruck:

    Davon habe ich gehört, konnte zu dem Zeitpunkt aber wenig mit anfangen. Jetzt wäre es bestimmt wieder interessant 🙂

    @camper sagte in C-Array an Konstruktor übergeben (rvalue reference?):

    ParamPack<T, I...> wird zu ParamPack<T, 0, 1, 2>
    ParamPack<T, I>... wird zu ParamPack<T, 0>, ParamPack<T, 1>, ParamPack<T, 2>

    Das leuchtet mir ein. Ich dachte der Expansionsoperator bezieht sich nur unmittelbar auf den Datentyp selbst. Aber ist ja interessant, dass er quasi in der Lage ist zu sehen, ParamPack selbst kann nicht direkt entpackt werden, aber das I, was Template parameter ist.

    Die Lösung gefällt mir auf jedenfall ganz gut und ich werde sie jetzt in meine Klasse einbauen. Nicht ganz so elegant wie std::initalizer_list, aber die ist meines Wissens nach in den Compiler eingebaut (Compiler generiertes Array) und daher nicht einfach so nachbaubar



  • Hab mir es nochmal angeschaut und mir ist aufgefallen, dass ich diese Lösung nur in einem partiell spezialisierten template brauche und irgendwie unschön diesen template parameter mit mir rumzuschleppen, obwohl ich ihn eig nur für den Konstruktor brauche der einen partiell spezialisierten Klasse.
    Daher habe ich nochmal überlegt, ob man das nicht doch als Funktionstemplate realisieren könnte ... der bisherige Lösungsansatz scheitert jedoch daran, dass man Funktionen nicht partiell spezialisieren kann.

    Meine Idee war daher das ganze als Funktionsparameter zu verpacken (der Einfachheithalber jetzt mal ohne die restlichen template parameter)

    template <uint8_t... I>
    void print(IndexSequence<I...>, ParamPack<uint8_t, I>... v) {
      for(auto i :  {v...}) {
        std::cout << (uint16_t) i << " ";
      }
      std::cout << '\n';
    }
    
    int main() {
      print(makeIndexSequence<3> {}, 1,2,3);
    }
    

    Wie mann sehen kann muss man makeIndexSequence<3> aber mit angegeben. Eine Möglichkeit das als default value anzugegeben ist mir nicht eingefallen. Hast du da noch ein paar Tricks auf Lager?😉
    Oder ist das Schwachsinn mit in die Funktion packen ... meines Verständnis nach wäre es da halt passender, da es ja nicht wirklich zur Klasse, sondern nur zur Funktion gehört.

    Und natürlich ist es auch immer eine Frage des Nutzen ... es muss ja zumindest besser sein als die Variante mit dem Code von ganz oben und einfach einem static_cast<T>.


  • Mod

    @Leon0402
    Gibt es eine wichtigen Grund, den Aufwand zu betreiben? Wenn du am Ende sowieso eine Initialisierungsliste benutzt (im for), warum diese nicht gleich als Funktionsparamter nutzen?



  • Das ist nur ein Beispiel gewesen.
    Ich will es nach wie vor nutzen, um ein Array zu initalisieren direkt in der initalizer list (siehe oben).
    Als konkreten Anwendungsfall: Einen mathematischen Vektor.
    Andere Anwendungsfälle sind natürlich denkbar, daher geht es allgemein um den Ansatz.

    Ich weiß nicht genau was du meinst? Eine std::initalizer_list? Die steht nicht zur Verfügung leider. Im Grunde will ich die ja nach Möglichkeit nachbauen. Nur ist das 1:1 nicht möglich, daher der Versuch das eben mit ein paar Tricks hinzubekommen 😉


  • Mod

    @Leon0402 sagte in C-Array an Konstruktor übergeben (rvalue reference?):

    Ich weiß nicht genau was du meinst? Eine std::initalizer_list? Die steht nicht zur Verfügung leider. Im Grunde will ich die ja nach Möglichkeit nachbauen. Nur ist das 1:1 nicht möglich, daher der Versuch das eben mit ein paar Tricks hinzubekommen 😉

    Das ist etwas verwirrend, weil du sie in deinen Beispielen mit verwendet hast. std::initializer_list exakt nachzubauen ist unmöglich, weil {expr...} in bestimmten Kontexten eben von vornherein dieser Typ ist.

    Nach dem Überschlafen der Frage ist mir aufgefallen, dass ein Deduction guide nicht unbedingt mit dieser Signatur als Konstruktor existieren muss. Also ist so etwas möglich:

    #include <type_traits>
    #include <utility>
    
    template <typename T, typename S>
    struct array_impl;
    
    template <typename T, std::size_t... I>
    struct array_impl<T, std::index_sequence<I...>> {
      constexpr array_impl(std::conditional_t<bool(I), T, T>... args) noexcept(noexcept(T{std::declval<T&>()}))
      : elems{args...} {}
      T elems[sizeof...(I)];
    };
    
    template <typename T, std::size_t N>
    struct array : array_impl<T, std::make_index_sequence<N>> {
      using array_impl<T, std::make_index_sequence<N>>::array_impl;
    };
    
    template <typename T, typename... U>
    array(T, U...) -> array<T, sizeof...(U) + 1>;
    
    template <typename T, std::size_t N>
    void foo(array<T, N>) {}
    
    int main() {
        array x = { 1, 2, 3 }; // -> array<int, 3>
        foo(array{(char)1,1}); // -> array<char, 2>
        //foo({0,0}); //nope
    }
    

    Hier wird der Elementtyp durch den ersten Initialisierer bestimmt. Leider etwas mehr Schreibaufwand in der Anwendung als man idealerweise haben möchte.



  • In der Tat etwas viel Schreibarbeit. Das Leben wäre deutlich einfacher, wenn man Parameter Packs zurückgeben könnte oder der operator ... sich auch auf das paramameter Pack in der index_sequence beziehen könnte oder man ein alias für parameter packs erstellen könnte

    //Per Funktion 
    void print(ParamPack<T,  makeIndexSequence<N>.getParamPack()>... args);
    //Wenn der ... direkt das int... in der IndexSequence erkennen würde 
    void print(ParamPack<T, makeIndexSequence<N>>... args); 
    //Per alias 
    void print(ParamPack<T, makeIndexSequence<N>::ParamPack>... args);
    

    Es gibt also einige sehr schöne Schreibweisen, um sowas zu realisieren. Leider wird keine davon unterstützt.


Anmelden zum Antworten