Array-Member initialisieren



  • Hi, ich habe folgendes Problem:

    class base { /* viele Konstruktoren */ };
    
    class intermediate : public base {
       std::array<...> member;
       std::array<...> foo;
    };
    
    class derived : public intermediate { /* soll selbe Konstruktoren haben wie base */ };
    

    derived hat keine Member, seine Konstruktoren sind daher trivial. Ich würde also gerne in derived die Konstruktoren von base erben. Ich kann nur Konstruktoren der jeweils nächsten Elternklasse erben, d.h. intermediate muss auch alle Konstruktoren von base erben, damit er sie an derived weiter vererben kann. Also:

    class intermediate : public base {
       std::array<...> member;
       std::array<...> foo;
    public:
       using base::base;
    };
    
    class derived : public intermediate {
    public:
       using intermediate::intermediate;
    };
    

    Jetzt bleibt das Problem, dass die Arrays nicht initialisiert sind. Wenn ich jetzt anfange, irgendwelche Konstruktoren zu schreiben, ist die gesamte Vererbung dahin (entweder man erbt alle Konstruktoren oder garkeine 😡 ) und ich muss in derived auch alle Konstruktoren ausschreiben. Es wäre also besser, ich könnte alle Konstruktoren schön vom Compiler generieren lassen, um Boilerplate zu sparen. Die Möglichkeit, die ich habe, ist natürlich in-class Initialization:

    class intermediate : public base {
       std::array<...> member{ }; // Value-Initialized
       std::array<...> foo{ ... }; // siehe unten
    public:
       using base::base;
    };
    

    So würde das funktionieren, aber jetzt kommt mein Problem:
    Das zweite Array soll in jedem Element einen Pointer auf das jeweilige Element im ersten Array besitzen (die Arraygrößen sind Template-Parameter, es muss also alles dynamisch sein).
    Ich habe mir folgendes Template-Meta-Programm dafür geschrieben:

    #include <array>
    
    template <typename T, int N, int... Ns>
    struct array_gen : public array_gen<T, N - 1, N - 1, Ns...> { };
    
    template <typename T, int... Ns>
    struct array_gen<T, 0, Ns...> {
    	static constexpr std::array<T*, sizeof...(Ns)> pointers(std::array<T, sizeof...(Ns)>& a) {
    		return { { &a[Ns]... } };
    	}
    };
    
    template <int N>
    struct intermediate {
    	using warum_das = array_gen<int, N>;
    
    	std::array<int, N> arr{ };
    	std::array<int*, N> a = warum_das::pointers(arr);
    };
    
    int main() {
    	intermediate<5> t;
    }
    

    Hier eine Frage: Warum muss ich einen Typ-Alias warum_das benutzen und kann nicht direkt array_gen<int, N>::pointers(arr) schreiben? Sowohl Visual Studio 2015 als auch der GCC murren, clang frisst es. Welcher Compiler ist verbuggt?

    Das würde funktionieren, aber es bricht in dem Moment zusammen, wenn ich kein Array von Pointern will, sondern ein Array von std::atomic<int*> möchte, denn Atomics können nicht kopiert werden. Was kann ich tun?


  • Mod

    Kompiliert in HEAD. (Der relevante Bug-report für GCC findet sich hier)

    Das wurde übrigens von CWG #325 behandelt; Die Problematik besteht darin, dass name lookup erst feststellen muss, dass std::array ein Template ist, sodass das Komma nicht einen neuen Deklarator (mit invalidem Initializer) einführt.

    Zu deiner zweiten Frage:

    §8.5.1/2 schrieb:

    Each member is copy-initialized from the corresponding initializer-clause.

    I.e.

    std::array<std::atomic<int*>> arr = {0, 0};
    

    initialisiert jedes Element via

    std::atomic<int*> elem = 0;
    

    Jedoch muss für diesen Fall eine temporäre Kopie erstellt und mit dieser initialisiert werden (§8.5/(17.6.2)). Das funktioniert offensichtlich nicht. Du kannst aber stattdessen eine eigene Array-Klasse basteln, die eine initializer_list<T> als Konstruktor-Argument nimmt und damit das Array befüllt.

    Also e.g. (unvollständig!):

    #include <array>
    #include <atomic>
    
    template <typename T, std::size_t N>
    struct Array : std::array<T, N> {
    	template <typename U>
    	constexpr Array(std::initializer_list<U> ilist) {
    		auto p = std::array<T, N>::begin();
    		for (auto iter = ilist.begin(); iter != ilist.end(); ++iter)
    			*p++ = std::move(*iter);
    	}
    };
    
    int main() {
    	int i;
    	Array<std::atomic<int*>, 2> arr = {(int*)nullptr, &i};
    }
    

    Ohne den Cast geht es übrigens nicht, da inkonsistente Typen in diesem Fall sofort in einem Fehler resultieren. Nicht besonders hübsch, aber naja.



  • Danke für deine Hilfe (und die Links).
    Wenn ich das mit meinem TMP-Schnipsel verbinde, gibt's aber wieder Probleme:

    #include <array>
    #include <atomic>
    #include <initializer_list>
    
    template <typename T, std::size_t N>
    struct my_array final : std::array<T, N> {
    	template <typename U>
    	constexpr my_array(std::initializer_list<U> list) {
    		auto iter = std::begin(*this);
    		for(auto& elem : list)
    			*iter++ = std::move(elem);
    	}
    };
    
    template <typename T, int N, int... Ns>
    struct array_gen : public array_gen<T, N - 1, N - 1, Ns...> { };
    
    template <typename T, int... Ns>
    struct array_gen<T, 0, Ns...> {
    	static constexpr std::initializer_list<T*> pointers(std::array<T, sizeof...(Ns)>& a) {
    		return { &a[Ns]... };
    	}
    };
    
    template <int N>
    struct test {
    	std::array<int, N> arr{ };
    	my_array<std::atomic<int*>, N> a{ array_gen<int, N>::pointers(arr) };
    };
    
    int main() {
    	test<5> t;
    }
    

    Jetzt wird das Template-Argument des Array-Konstruktors selbst als initializer_list aufgelöst, so ein Blödsinn, da der Compiler sinnigerweise bei braced initialization den verfügbaren Initializer-List-Konstruktor aufruft:

    int main() {
       std::array<int, 3> arr{ };
       my_array<std::atomic<int*>, 3> a{array_gen<int, 3>::pointers(arr)}; // klappt nicht
       my_array<std::atomic<int*>, 3> a(array_gen<int, 3>::pointers(arr)); // klappt
    };
    

    Im ersten Fall sieht der Compiler nur: Braced initialization, nehme initializer_list -Konstruktor, löse Parameter nach decltype(array_gen<int, 3>::pointers(arr)) auf, im zweiten Fall löst er den Template-Parameter richtig auf. Ich kann aber nur die erste Syntax für in-class initialization nutzen und für einen Template-Konstruktor kann man keine expliziten Template-Parameter angeben. Langsam nervt mich C++ mit seinen dämlichen initializer_list/uniform initialization-Konflikten. 😡



  • Naja, da ich dieses eigene Array eh niemals außer hier brauche, kann ich das Problem umschiffen, indem ich es auf Atomics festnagel und das Template entferne:

    template <typename T, std::size_t N>
    struct my_array final : std::array<std::atomic<T>, N> {
    	constexpr my_array(std::initializer_list<T> list) {
    		auto iter = std::begin(*this);
    		for(auto& elem : list)
    			*iter++ = std::move(elem);
    	}
    };
    

    So kompiliert es und macht, was es soll.


  • Mod

    Huch, da fällt mir was viel hübscheres ein:

    template <std::size_t N, typename=std::make_index_sequence<N>>
    struct test {};
    
    template <std::size_t N, std::size_t... indices>
    struct test<N, std::index_sequence<indices...>> {
        std::array<int, N> arr{ };
        std::array<std::atomic<int*>, N> a{{&arr[indices]}...};
    };
    

    wusste gar nicht, dass {…} ein gültiges pattern ist. Falls du noch nicht mit C++14 arbeiten kannst, lässt sich make_index_sequence auch leicht selbst implementieren.

    PS: Explizite instantiierungen gehen auch direkter:

    template struct test<5>;
    


  • Arcoth schrieb:

    Huch, da fällt mir was viel hübscheres ein:

    In der Tat, index_sequence kannte ich noch garnicht, bzw. habe es selbst gebastelt.



  • Hm, zu früh gefreut, es kompiliert nicht:

    #include <array>
    #include <atomic>
    #include <utility>
    
    template <std::size_t N, typename = std::make_index_sequence<N>>
    struct test {};
    
    template <std::size_t N, std::size_t... indices>
    struct test<N, std::index_sequence<indices...>> {
    	std::array<int, N> arr{ };
    	std::array<std::atomic<int*>, N> a{ { &arr[indices] }... };
    };
    
    int main() {
    	test<5> t;
    }
    

    Jedenfalls beschweren sich Clang und GCC, VS nimmt es, aber sicher nicht standardkonform. Ich muss also trotzdem mein Array von oben benutzen, also:

    #include <array>
    #include <atomic>
    #include <utility>
    
    template <std::size_t N, typename = std::make_index_sequence<N>>
    struct test {};
    
    template <typename T, std::size_t N>
    struct atomic_array : public std::array<std::atomic<T>, N> {
    	atomic_array(std::initializer_list<T> list) {
    		auto iter = std::begin(*this);
    		for(auto& elem : list)
    			*iter++ = std::move(elem);
    	}
    };
    
    template <std::size_t N, std::size_t... indices>
    struct test<N, std::index_sequence<indices...>> {
    	std::array<int, N> arr{ };
    	atomic_array<int*, N> a{ { &arr[indices] }... };
    };
    
    int main() {
    	test<5> t;
    }
    

    So geht es (endlich)!


  • Mod

    Edit: Alles Quatsch. Habe lediglich einen Flüchtigkeitsfehler eingebaut 🤡
    So funktionierts:

    std::array<std::atomic<int*>, N> a {{ { &arr[indices] }... }};
    


  • Warum kann ich std::array<std::atomic<int*>, N> mit einer std::initializer_list<std::initializer_list<int*>> initialisieren?


  • Mod

    Jodocus schrieb:

    Warum kann ich std::array<std::atomic<int*>, N> mit einer std::initializer_list<std::initializer_list<int*>> initialisieren?

    Weil kein entsprechender Konstruktor vorhanden ist? Du kannst lediglich Klassen mit einem initializer_list<> Objekt initialisieren wenn ein entsprechender Konstruktor vorhanden ist. Aggregate haben keine vom User bereitgestellten Konstruktoren. Was in meinem (jetzt endlich) korrigierten Code verwendet wird, verwendet initializer_list gar nicht.



  • Ich hab gefragt, warum ich es kann, da ich deinen Code so interpretiere (und eben array keinen solchen Konstruktor hat). Und da sind keine Initializer_lists drin? Was ist dann der Unterschied zwischen

    std::array<std::atomic<int*>, N> a{ &arr[indices]... };
    
    std::array<std::atomic<int*>, N> a{{ &arr[indices]... }};
    

    und

    std::array<std::atomic<int*>, N> a{{ { &arr[indices] }... }};
    

    ?
    Das erste ist die normale Aggregat-Initialisierung. Das zweite ist eine Aggregat-Initialisierung eines temporären Arrays, das dann kopiert wird? Und das dritte?


  • Mod

    std::array<std::atomic<int*>, N> a{ &arr[indices]... };
    

    initialisiert jedes Element e n via

    std::atomic<int*> e_n = &arr[n];
    

    Diese Form der initialisierung von a ist tatsächlich equivalent zu1

    std::array<std::atomic<int*>, N> a{{ &arr[indices]... }};
    

    Jedoch können die Klammern in diesem Fall weggelassen werden. In keinem der beiden Fälle wird ein temporäres Array oder array erstellt. Es werden lediglich Temporaries durch copy-initialization benötigt. Dir ist sicherlich bekannt, dass auch in

    std::string s = "abc";
    

    eine Temporary erstellt wird?

    Der Große Unterschied der nun in der dritten Variante ausgenutzt wird, ist dass

    std::array<std::atomic<int*>, N> a{{ { &arr[indices] }... }};
    

    Eine Initialisierung a la

    std::atomic<int*> e_n = {&arr[n]};
    

    impliziert - aber hier brauchen wir - dank der Regeln von list-initialization - keine Temporary, denn der Konstruktor von e n wird direkt mit &arr[n] aufgerufen.

    Wenn wir jedoch

    std::array<std::atomic<int*>, N> a{ { &arr[indices] }... };
    

    schreiben, dann wird zuallererst versucht, dass interne Array mit { &arr[0] } zu initialisieren, was natürlich fehlschlägt.
    ---
    1 Ich bin mir gerade nicht einmal sicher, ob die erste Variante standardkonform ist, da §8.5.1/11 was anderes suggeriert, aber da alle mir bekannten Implementierungen es per se annehmen, scheine ich da etwas falsch zu interpretieren



  • Arcoth schrieb:

    Eine Initialisierung a la

    std::atomic<int*> e_n = {&arr[n]};
    

    impliziert - aber hier brauchen wir - dank der Regeln von list-initialization - keine Temporary, denn der Konstruktor von e n wird direkt mit &arr[n] aufgerufen.

    Meinst du nicht eher, dass das

    std::atomic<int*> e_n{&arr[n]}; // ohne =
    

    impliziert? Gemäß http://en.cppreference.com/w/cpp/language/list_initialization wäre das mit dem = eine copy-list initialization und nicht direct.


  • Mod

    Beide. In jeder Form von list-initialization werden die initializer-clauses stets direkt an den Konstruktor weitergeleitet - außer ein initializer_list Konstruktor wird aufgerufen, was hier aber offensichtlich nicht der Fall ist.

    Edit: Was genau meinste gerade? Bin etwas verwirrt. Dass die Arrayelemente bei Aggregatinitialisierung stets mittels copy-initialization initialisiert werden, habe ich bereits aufgezeigt.



  • Arcoth schrieb:

    Bin etwas verwirrt.

    Hat sich erledigt, ich hab mir die Regeln im Standard noch mal durchgelesen. Danke!


  • Mod

    Hab' auch mein vorhin erwähntes Problem gelöst:

    1 Ich bin mir gerade nicht einmal sicher, ob die erste Variante standardkonform ist, da §8.5.1/11 was anderes suggeriert, aber da alle mir bekannten Implementierungen es per se annehmen, scheine ich da etwas falsch zu interpretieren

    Dieser Paragraph wurde durch CWG #1270 angepasst, sodass

    std::array<int, 1> arr{0};
    

    seit C++14 gültig ist.



  • Arcoth schrieb:

    Edit: Was genau meinste gerade? Bin etwas verwirrt. Dass die Arrayelemente bei Aggregatinitialisierung stets mittels copy-initialization initialisiert werden, habe ich bereits aufgezeigt.

    Was ich meinte, ist, dass eine List-Initialization à la

    std::atomic<int*> e_n = {&arr[n]};
    

    Auch ein Temporary erzeugt, im Gegensatz zu

    std::atomic<int*> e_n{&arr[n]};
    

    , wobei letzteres wohl bei

    std::array<std::atomic<int*>, N> a{{ { &arr[indices] }... }};
    

    passiert.


Anmelden zum Antworten