Wie definiert man eine constexpr std::initializer_list?



  • Beim Durchsehen des Codes des Matrix Beispiel sehe ich, dass std::initializer_list.size constexpr ist. D.h. es muss möglich sein eine initializer_list constexpr zu definieren.

    Das Problem lässt sich auf folgendes kurzes Beispiel reduzieren, welches den Beispielen im Netz folgt.

    import <initializer_list>;
    import <cstdlib>;
    
    int main() {
        constexpr std::initializer_list<unsigned int> IL16 {2u};
    
        return EXIT_SUCCESS;
    }
    

    Folgender Compileraufruf (gcc 11.3.0, Module werden vom Makefile vorher übersetzt)

    g++-11 -std=c++20 -O3 -Wpedantic -pedantic-errors -fno-gnu-keywords -Wall -Wextra -Wplacement-new=2 -Waligned-new -Wdouble-promotion -Winit-self -Wnoexcept -Wold-style-cast -fmodules-ts test.cc -o test
    

    liefert nun diesen Fehlercode

    test.cc: In Funktion »int main()«:
    test.cc:5:59: Fehler: »const std::initializer_list<unsigned int>{((const unsigned int*)(&<anonym>)), 1}« ist kein Konstantenausdruck
        5 |     constexpr std::initializer_list<unsigned int> IL16 {2u};
          |  
    

    Das lässt mich ehrlich gesagt ratlos zurück. Was ist an dem Ausdruck {2u} denn nun nicht constexpr? Und wenn der Compiler dynamisch die IL initialisiert, wie soll es dann möglich sein je so eine constexpr IL zu definieren?



  • Mit modules habe ich noch nicht rum probiert, aber MSVC kompiliert das, wenn ich auf die "altmodischen" Header umstelle.

    Edit: Mit den import Anweisungen bekomme ich lustige andere Fehler... muss mich damit wohl mal genauer auseinander setzen, vlt muss ich dafür die Projekteinstellungen bei VS noch ändern.



  • Ok danke, es scheint also ein Problem des GCC zu sein. Ich werde es mal mit clang++ probieren.

    Was das Thema imports anbelangt, man muss bei einigen Compiler (z.B. GCC) die Header vorher extra übersetzen, und man muss zusätzliche Argumente beim übersetzen angeben. Hier ist das -fmodules-ts sonst akzeptiert er den Code nicht.



  • Mit dem clang++ 14.0.6 gibt es ebenfalls einen Fehler

    test.cc:5:57: error: constexpr variable 'IL16' must be initialized by a constant expression
        constexpr std::initializer_list<const unsigned int> IL16 {2u};
                                                            ^    ~~~~
    test.cc:5:57: note: pointer to subobject of temporary is not a constant expression
    test.cc:5:62: note: temporary created here
        constexpr std::initializer_list<const unsigned int> IL16 {2u};
                                                                 ^
    1 error generated.
    

    Na denn, warten wir auf die nächsten Compiler Releases.



  • Mach das Ding static?



  • Das löst das Problem nicht, da es darum geht, dass man für Konstruktoren die Foo foo = {0, 5, 5}; Schreibweise nutzen kann. GCC und clang erlauben das zurzeit nicht, weil sie temporäre Zeiger anlegen und die dann nicht constexpr sind.


  • Mod

    Es ist unter den bestehenden Regeln nicht legal. Da wir hier keine Referenz haben, ist die full-expression ein prvalue:

    A constant expression is either a glvalue core constant expression that refers to an entity that is a permitted result of a constant expression (as defined below), or a prvalue core constant expression whose value satisfies the following constraints:
    (11.1) if the value is an object of class type, each non-static data member of reference type refers to an entity that is a permitted result of a constant expression,
    (11.2) if the value is of pointer type, it contains the address of an object with static storage duration, the address past the end of such an object ([expr.add]), the address of a non-immediate function, or a null pointer value,
    (11.3) if the value is of pointer-to-member-function type, it does not designate an immediate function, and
    (11.4) if the value is an object of class or array type, each subobject satisfies these constraints for the value.
    An entity is a permitted result of a constant expression if it is an object with static storage duration that either is not a temporary object or is a temporary object whose value satisfies the above constraints, or if it is a non-immediate function.

    Der Haken ist, dass initializer_list irgendwie auf das interne array verweisen muss, dessen lifetime aber durch die der initializer_list Variable vorgegeben ist. Clang kann hierzu auch eine aufschlussreichere Diagnostik liefern.

    Jetzt kann man aber Zwei und Zwei zusammen zaehlen und folgern, dass

    static constexpr std::initializer_list<unsigned int> IL16 {2u};
    

    Wohlgeformt sein muss, weil das Array ja static storage duration hat. Und, siehe da......

    @john-0 sagte in Wie definiert man eine constexpr std::initializer_list?:

    Das löst das Problem nicht, da es darum geht, dass man für Konstruktoren die Foo foo = {0, 5, 5}; Schreibweise nutzen kann. GCC und clang erlauben das zurzeit nicht, weil sie temporäre Zeiger anlegen und die dann nicht constexpr sind.

    Funktioniert doch? https://gcc.godbolt.org/z/zj94Y37Mb
    Oder was meinst Du? Solange das Array nur zur Konstruktion dientund nicht als Ergebnis einer constant expression, gibt es kein Problem.



  • Es geht um das Problem in Zeile 24. Die Alternative in Zeile 25 funktioniert zwar (War der Hinweis darauf nicht von Dir?), ist aber letztlich nur ein Workaround um eine Lücke in der Norm. Ich dachte erst es sei ein Compiler Bug, aber folgender Beitrag von Jason Turner zeigt dann, dass es kein Bug ist sondern die Norm das noch nicht vorsieht. Man lernt in Bezug auf C++ nie aus. Intuitiv ist das jedenfalls nicht, denn wie man leicht nachschauen kann siehe dazu CppReference, ist die Member Function size() natürlich constexpr definiert. Das lässt jetzt einem verwundert zurück, weil der Ausdruck {2u} natürlich trivial constexpr ist. Aber die Abgründe der Norm und der Compilerimplementation lassen da die Möglichkeit zu, dass der Compiler ein Temporary generiert, und das dann nicht constexpr ist. Uff, so sei es. Jedenfalls weiß ich nun warum es nicht geht.

    Danke noch mal an alle.

    #include <initializer_list>
    #include <cstdlib>
    #include <iostream>
    #include <concepts>
    
    template <typename T> concept Integer = std::integral<T> && (! std::same_as<T, bool>);
    template <typename T> concept SignedInteger = std::signed_integral<T> && (! std::same_as<T, bool>);
    template <typename T> concept UnsignedInteger = std::unsigned_integral<T> && (! std::same_as<T, bool>);
    
    template<typename IndexType>
    class Bounds {
    public:
        consteval Bounds (const std::initializer_list<const IndexType>&);
    };
    
    template <typename IndexType>
    requires UnsignedInteger<IndexType>
    class Bounds<IndexType> {
    public:
        static constexpr IndexType lower_ = 0;
        IndexType upper_;
    
        constexpr Bounds (const std::initializer_list<const IndexType>& list) {
            static_assert (1 != list.size());
            //if (1 != list.size()) throw std::logic_error ("Bounds<IndexType=unsigned_integral> needs one Element exactly");
    
            auto it = list.begin();
            upper_ = *it;
        }
    };
    
    int main() {
        Bounds<unsigned int> ub = {2u};
    
        return EXIT_SUCCESS;
    }
    

  • Mod

    Erstens war das nicht im Ansatz die im OP gestellte Frage.

    Zweitens... das "Problem in Zeile 24" ist kein Problem, sondern unsinniger Code.

    Ich habe schon vielmals, u.a. Dir, wenn ich mich recht erinnere, erklaeren muessen, dass Funktionsparameter von constexpr Funktionen keine Konstanten sein koennen, weil sie dann die Typisierung der Funktion verunmoeglichen. (Und auch aus 100 anderen Gruenden ist es Quatsch, u.a. weil die Funktion mit unterschiedlichen Aktualparametern ausgefuehrt wird, und der Wert dann offensichtlich variiert.)

    constexpr void f(int i) {
       array<int, i> a; // ersetze i durch mylist.size(), same story. f waere hiermit implizit zum Template geworden.
       // (Es spielt hierbei keine Rolle, ob f sowieso eine templated entity ist, weil es eine Memberfunktion eines Templates ist, es ergibt ebenso wenig Sinn).
    }
    

    i ist offensichtlich variabel und nicht konstant (sonst waere es ja kein Parameter...) und kann deshalb nicht als konstant verwendet werden. Das wird sich niemals aendern, weil es Schwachsinn ist, nicht dank der "Abgründe der Norm und der Compilerimplementation". Es hat auch nichts damit zu tun, ob der referee des Parameters static storage duration hat, wie sich durch ein entsprechendes Beispiel zeigen laesst.

    Ich bin mir nicht sicher, was der Zweck deines Beispiels ueberhaupt sein mag. Es ist schon im weiteren Sinne komisch, einen initializer-list ctor zu definieren, dessen Rumpf statisch von der Laenge abhaengt. In dem Fall boete es sich mehr an, ein const-ref Array zu nehmen (was uebrigens auch list-initialization unterstuetzt).



  • @Columbo sagte in Wie definiert man eine constexpr std::initializer_list?:

    Ich habe schon vielmals, u.a. Dir, wenn ich mich recht erinnere, erklaeren muessen, dass Funktionsparameter von constexpr Funktionen keine Konstanten sein koennen, weil sie dann die Typisierung der Funktion verunmoeglichen.

    Jede Konstante in C++ Programm hat natürlich einen wohl definierten Typen. Seit C++11 wurde sehr viel Aufwand betrieben, dass man durch Suffixe die Typen für Literale und somit Konstanten definieren kann. Natürlich kann eine inline Funktion korrekt typisiert werden, wenn an sie einen constexpr Parameter übergeben bekommt. Der Compiler muss ebenfalls wie im Falle, dass der Parameter auto ist, für den jeweiligen Fall Code generieren. Wir haben also längst den Fall, dass der Compiler implizite Templates generiert. Natürlich wird dann eine solche Funktion für echte Variablen nicht mehr nutzbar, aber in meinem konkreten Fall ist das ja explizit so gewünscht.

    #include <array>
    #include <iostream>
    constexpr void f(auto i) { std::cout << i << std::endl; }
    
    int main () {
        int i = 5; std::string s = "Hello World!";
        f(i); f(s);
    }
    

    Das wird sich niemals aendern,

    Du hättest Dir das verlinkte Video wirklich anschauen sollen. Es ist in Arbeit.


  • Mod

    Du hättest Dir das verlinkte Video wirklich anschauen sollen. Es ist in Arbeit.

    🙄 Vielleicht haettest Du es Dir anschauen sollen? Da wird doch praezise erklaert, was ich bereits erklaert habe: Es geht nicht mit Funktionsparametern, sondern mit Templateparametern, was auch kein Novum ist, da es seit 30 Jahren geht; die einzige Neuerung, die Jason dort in seinem 7-minuten-langen-aber-haette-auch-eine-halbe-seite-sein-koennen Vortrag zeigt, ist ein komplexer non-type Parameter. Es schreibt sogar noch jemand in den Kommentaren, dass das, was Du meinst, eben nicht vorgestellt wurde:

    I hoped it would be something like this:
    auto do_something(constexpr const Vector& vec);

    Und Jason, der ja mit seinem nach Clickbait anmutenden Video die versprochenen constexpr Parameter gar nicht liefert, muss ihm dann ein ausweichendes "vielleicht in 5 Jahren" antworten.

    Natürlich kann eine inline Funktion korrekt typisiert werden, wenn an sie einen constexpr Parameter übergeben bekommt. Der Compiler muss ebenfalls wie im Falle, dass der Parameter auto ist, für den jeweiligen Fall Code generieren.

    Das habe ich doch bereits ausfuehrlich gesagt: Eine Funktion mehrfach typisieren zu muessen, macht sie zu einem Template. Warum statt Templateparametern "constexpr Parameter"?

    Wir haben also längst den Fall, dass der Compiler implizite Templates generiert.

    Daran ist nichts "implizit". Der Compiler und Leser kann in einer Definition

    void f(Concept auto);
    

    an dem placeholder auto in der Signatur schluessig erkennen, dass f ein abbreviated function template ist. An der Signatur einer Funktion mit

    constexpr Bounds (const std::initializer_list<const IndexType>& list)
    

    nicht. An diesem Punkt werde ich auch die ellenlangen Diskussionen erwaehnen, die LWG/CWG dazu hatten. Siehe bspw. ein Proposal dass dieses Feature wieder entfernen wollte: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0696r0.html

    The abbreviated function and template introducer syntax features defined in the Concepts TS [Concepts] have proven to be controversial as evidenced by discussion within the committee reflectors [ExploringConcepts] and the P0587R0 [P0587R0] and P0464R2 [P0464R2] paper submissions. This paper proposes removing these features from the Concepts TS with the goal of increasing consensus on adopting the remaining Concepts TS functionality into the current working paper.

    Das war also mit Sicherheit kein leichtfertiger Schritt. Was Du vorschlaegst, ist allerdings aequivalent dazu, jede Funktion zu einem potenziellen Template zu machen, sobald irgendein Parameter in einem constexpr Kontext benoetigt wird. Das ist Quatsch und wuerde niemals Absegnung finden.

    Solltest Du (entgegen der in diesem Thread von Dir gezeigten Beispiele) meinen, dass man constexpr-qualifizierte Funktionsparameter als syntaktisch differenzierten Template-Parameter einsetzen sollte, dann verneine ich ebenfalls: Das hat nur Nachteile gegenueber einer konventionellen Deklaration als non-type Template Parameter, und fuehrt 100%-ig zu Komplikationen in der ganzen Sprache.



  • @Columbo sagte in Wie definiert man eine constexpr std::initializer_list?:

    Warum statt Templateparametern "constexpr Parameter"?

    Weil der Compiler es Dir nur in dieser Form vor die Füße kippt! Man hat da keinerlei Wahlmöglichkeit. Will man einen Konstruktor schreiben, der eine eine Liste {value1, value2, …} verarbeitet muss man das so – und nur so machen. Natürlich hätten die Autoren der Norm die Wahlmöglichkeit gehabt das anders festzulegen. Nur das haben sie halt nicht.

    Man sieht das sehr gut, wenn man

    #include <initializer_list>
    #include <iostream>
    #include <cstdlib>
    
    #include "demangle.h" // für demangleType(), gibt den Namen der Klasse lesbar aus
    
    int main() {
        auto il = {11u, 12u, 13u};
    
        std::cout << "il " << demangleType(il) << "\n";
        std::cout << std::endl;
    
        return EXIT_SUCCESS;
    }
    

    schreibt, dann erhält man dazu die passende Ausgabe:

    il std::initializer_list<unsigned int>
    

    Was ist das Ziel?
    Es geht darum, dass man constexpr Objekte mit einem constexpr Konstruktor typsicher konstruieren kann.

    constexpr Matrix M = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    

    Diese Zeile geht aktuell auch noch nicht, weil verschachtelte std::initializer_lists nicht erkannt werden. Außer man definiert dann den Konstruktor mit verschachtelter std::initializer_list. Schöner wäre es, wenn man in der Lage wäre auf der rechten Seite durch einfacher Formatierung die Listen zu verschachteln, so dass der Compiler den Typ direkt erkennen könnte.

    Es ist ehrlich gesagt nicht einzusehen, weshalb der Compiler nicht in der Lage sein sollte Objekte die die Bedingungen für constexpr erfüllen nicht schon zum Übersetzungszeitpunkt typsicher zu initialisieren. Aktuell geht das nicht. Irgendwelche cast Orgien braucht auch kein Mensch. Mir persönlich ist es so was von egal wie das konkret passiert, solange das ganze funktioniert. Es bestätigt nur meinen Eindruck, dass viele der seit C++11 eingeführten Neuerungen nicht wirklich ausgereift sind.


  • Mod

    @john-0 Man kann Dir schwer folgen, wenn Du keine konkreten Problemstellungen in Form von Code zeigst. Zeig mal eine Syntax + Anforderungen mit Minimalbeispiel, dann kann man diskutieren, wie das aktuell implementierbar ist.

    Was irgendwas davon mit Typsicherheit zu tun hat ist mir auch ein Raetsel. Auf jeden Fall ist ein initializer-list ctor der nur eine fixe Menge von Elementen nehmen will, besser mit einer assertion bedient. Dadurch funktioniert das ganze auch einwandfrei mit Laufzeitauswertung, wuerde aber insbesondere bei constinit/constexpr Variablen dann einen Uebersetzungsfehler generieren.


  • Mod

    Ich glaube Du meinst folgendes:

    // Array of N elements (where N is constant)
    struct A {
        // members
        A(initializer_list<int> i) { /* bla */ }
    };
    
    A a = {1, 2, 3}; // Fehler, wenn 3 > N (oder ggf. 3 != N)
    

    In diesem Fall gibt es eine Reihe von Moeglichkeiten.

    • struct X {
          // garantiert, dass der assert einen Uebersetzungsfehler produziert
          consteval X(std::initializer_list<int> arr) { assert(arr.size() == 4); }
      }; 
      
      int main() {
          X x{1, 2, 3}; // Fehler
      }
      
    • A kann als Aggregat definiert werden, genau wie std::array (in welchem Fall sich A sowieso eruebrigt.)
    • A kann einen ctor mit const int (&arr)[5] definieren, der eine Syntax a la A a{{1, 2, 3}}; erlaubt. Das ist nicht wunderschoen, aber funktioniert, und ist sicher.


  • @Columbo sagte in Wie definiert man eine constexpr std::initializer_list?:

    @john-0 Man kann Dir schwer folgen, wenn Du keine konkreten Problemstellungen in Form von Code zeigst.

    Der notwendige Code steht doch einige Beiträge weiter vorne.

    #include <initializer_list>
    #include <cstdlib>
    #include <iostream>
    #include <concepts>
    
    template <typename T> concept Integer = std::integral<T> && (! std::same_as<T, bool>);
    template <typename T> concept SignedInteger = std::signed_integral<T> && (! std::same_as<T, bool>);
    template <typename T> concept UnsignedInteger = std::unsigned_integral<T> && (! std::same_as<T, bool>);
    
    template<typename IndexType>
    class Bounds {
    public:
        consteval Bounds (const std::initializer_list<const IndexType>&);
    };
    
    template <typename IndexType>
    requires UnsignedInteger<IndexType>
    class Bounds<IndexType> {
    public:
        static constexpr IndexType lower_ = 0;
        IndexType upper_;
    
        constexpr Bounds (const std::initializer_list<const IndexType>& list) {
            static_assert (1 != list.size());
            //if (1 != list.size()) throw std::logic_error ("Bounds<IndexType=unsigned_integral> needs one Element exactly");
    
            auto it = list.begin();
            upper_ = *it;
        }
    };
    
    int main() {
        Bounds<unsigned int> ub = {2u};
    
        return EXIT_SUCCESS;
    }
    

    Was irgendwas davon mit Typsicherheit zu tun hat ist mir auch ein Raetsel. Auf jeden Fall ist ein initializer-list ctor der nur eine fixe Menge von Elementen nehmen will, besser mit einer assertion bedient. Dadurch funktioniert das ganze auch einwandfrei mit Laufzeitauswertung, wuerde aber insbesondere bei constinit/constexpr Variablen dann einen Uebersetzungsfehler generieren.

    Der Witz ist doch gerade, dass ich danach frage, ob es zum Übersetzungszeitpunkt geht. Dass man dass per assertion, exception etc. zur Laufzeit prüfen kann ist mir bewusst. Und ich bevorzuge dann lieber eine Exception (die auch auskommentiert im Code steht), da die nicht per NDEBUG Define ausgehebelt wird.

    @Columbo sagte in Wie definiert man eine constexpr std::initializer_list?:

      struct X {
          // garantiert, dass der assert einen Uebersetzungsfehler produziert
          consteval X(std::initializer_list<int> arr) { assert(arr.size() == 4); }
     }; 
      
      int main() {
          X x{1, 2, 3}; // Fehler
      }
      ```
    

    Das funktioniert gar nicht, da assert keinen konstanten Ausdruck zurückliefert,d .h. es schlägt immer fehl und nicht nur wenn die falsche Anzahl Objekte enthalten ist. Für consteval muss man static_assert verwenden, und das geht nicht, weil der Compiler ein temporäres Objekt erzeugt. D.h. zurzeit kann man keinen Konstruktor schreiben, der wirklich zum Übersetzungszeitpunkt eine initializer_list erhält und zum Übersetzungszeitpunkt daraus ein constexpr Objekt macht.


  • Mod

    @john-0 sagte in Wie definiert man eine constexpr std::initializer_list?:

    @Columbo sagte in Wie definiert man eine constexpr std::initializer_list?:

      struct X {
          // garantiert, dass der assert einen Uebersetzungsfehler produziert
          consteval X(std::initializer_list<int> arr) { assert(arr.size() == 4); }
     }; 
      
      int main() {
          X x{1, 2, 3}; // Fehler
      }
      ```
    

    Das funktioniert gar nicht, da assert keinen konstanten Ausdruck zurückliefert,d .h. es schlägt immer fehl und nicht nur wenn die falsche Anzahl Objekte enthalten ist. Für consteval muss man static_assert verwenden, und das geht nicht, weil der Compiler ein temporäres Objekt erzeugt. D.h. zurzeit kann man keinen Konstruktor schreiben, der wirklich zum Übersetzungszeitpunkt eine initializer_list erhält und zum Übersetzungszeitpunkt daraus ein constexpr Objekt macht.

    Falsch. Keine Ahnung wie Du ueberhaupt darauf kommst, Du hast den Code offensichtlich nicht mal getestet (https://coliru.stacked-crooked.com/a/53c9a90bfbb4a423). Das hat in der Praxis schon immer funktioniert und seit C++17 garantiert, was consteval sowieso erfordert.

    Edit: Wahrscheinlich hast Du den static_assert in Deinem Code durch assert ersetzt, das Schlug fehl weil die Laenge der Liste 1 ist, aber du hast faelschlicherweise deduziert, dass es am fehlenden constexpr von __assert_fail liegt 🤣
    Guck mal hier: https://coliru.stacked-crooked.com/a/51003882390b9d36