constexpr function Ergebnis in einer aufrufenden constexpr function als Template-Argument nutzen



  • Hiho,

    im nachfolgendem beispielhaften Code soll eine constexpr-Funktion eine andere constexpr-Funktion aufrufen und das Ergebnis (size_t) als Template-Parameter für einen anderen Typ nutzen. So wie der Code unten steht, führt das ganze zu einem Compile-Fehler, weil das Ergebnis des Aufrufs von 'length' innerhalb von 'pstring' dann nicht mehr constexpr ist. Gibt es einen einfachen Weg, eine elegante Lösung, wie ich den Code unten abändern kann, sodass die gewünschte Verhaltensweise erreicht wird?

    #include <stdio.h>
    
    //
    template<typename T> constexpr size_t length(const T* s) {
        return *s ? 1 + length(s + 1) : 0;
    }
    
    //
    template<typename T, size_t N>class array{
        T data[N];
    public:
        constexpr T& operator[](size_t i)noexcept { return data[i]; }
        constexpr const T& operator[](size_t i)const noexcept { return data[i]; }
    };
    
    // Mache Pascal-String mit Längenbyte davor
    template<typename T>constexpr auto pstring(const T* s) {
        constexpr auto N = length(s);
        array<T, N + 1> A = { N };
        for (size_t i = 0; i < N; i++) {
            A[i + 1] = s[i];
        }
        return A;
    }
    
    constexpr auto len = length("Hallo"); // geht
    constexpr auto str = pstring("Hallo");    // geht nicht
    
    int main() {
        printf("%d,%s,%d", *str, str + 1, len);
    }
    

    VG



  • Kompilierst du noch mit C++11? Seit C++14 sind auch Aufrufe von constexpr-Funktionen erlaubt, s. z.B. constexpr Functions ("C++14").

    Ab C++20 ist man mit consteval sogar noch flexibler.



  • @Th69 sagte in constexpr function Ergebnis in einer aufrufenden constexpr function als Template-Argument nutzen:

    Kompilierst du noch mit C++11? Seit C++14 sind auch Aufrufe von constexpr-Funktionen erlaubt, s. z.B. constexpr Functions ("C++14").

    Ab C++20 ist man mit consteval sogar noch flexibler.

    Das Problem ist etwas komplexer. 😉 Da muss man erstmal nachhaken was was sein soll.

    @Pellaeon Das stdio C++ include ist #include <cstdio>. Was genau möchtest du mit dem array<T, N + 1> A = { N }; bezwecken? Möchtest du dass alle Felder von data mit N ausgefüllt werden, oder nur das erste? Beides wir so oder so nicht gehen. Denn data ist einerseits private und anderseits weiß der Compiler nicht, wie er eine brace enclosed initializer list mit einem Element in ein variable length array konvertieren soll. Außerdem ist das N in constexpr auto N = length(s); in diesem Kontext nicht das Gleiche wie ein non-type template parameter. Die beiden array subscribe operators in der array Klasse noexcept zu machen ist auch gewagt. Du wirst in der arrray Klasse auch constexpr Konstruktor benötigen.



  • @VLSI_Akiko sagte in constexpr function Ergebnis in einer aufrufenden constexpr function als Template-Argument nutzen:

    @Pellaeon Das stdio C++ include ist #include <cstdio>. Was genau möchtest du mit dem array<T, N + 1> A = { N }; bezwecken? Möchtest du dass alle Felder von data mit N ausgefüllt werden, oder nur das erste? Beides wir so oder so nicht gehen. Denn data ist einerseits private und anderseits weiß der Compiler nicht, wie er eine brace enclosed initializer list mit einem Element in ein variable length array konvertieren soll.

    Man möge mich korrigieren, aber meines erachtens sollte der C-Array Data Member nur public gemacht werden müssen, damit man das auf diese Weise via Aggregate initialization initialisieren kann. Die übrigen Array-Elemente sollten dann value-initialisiert werden, womit man dann ein { N, 0, 0, 0, ... }-Array bekäme.

    Außerdem ist das N in constexpr auto N = length(s); in diesem Kontext nicht das Gleiche wie ein non-type template parameter.

    Auch hier möge man ich korrigieren, aber ich sehe das Problem hier eher darin, dass der übergebene const char*-Parameter selbst in diesem Kontext constexpr-Ausdruck (mehr) ist. constexpr sizet N = 5; sollte eigentlich funktionieren. Leider hab ich da auch keine schnelle Lösung parat, wie man sowas kurz und elegant handhabt - hab noch keinen constexpr-String implementiert.

    Die beiden array subscribe operators in der array Klasse noexcept zu machen ist auch gewagt.

    Warum? Geben die nicht nur eine Referenz auf ein Array-Element zurück?

    Du wirst in der arrray Klasse auch constexpr Konstruktor benötigen.

    Nicht unbedingt, siehe oben: public: T data[N].



  • @Finnegan sagte in constexpr function Ergebnis in einer aufrufenden constexpr function als Template-Argument nutzen:

    Man möge mich korrigieren, aber meines erachtens sollte der C-Array Data Member nur public gemacht werden müssen, damit man das auf diese Weise via Aggregate initialization initialisieren kann. Die übrigen Array-Elemente sollten dann value-initialisiert werden, womit man dann ein { N, 0, 0, 0, ... }-Array bekäme.

    Das wäre eine Lösung.

    Auch hier möge man ich korrigieren, aber ich sehe das Problem hier eher darin, dass der übergebene const char*-Parameter selbst in diesem Kontext constexpr-Ausdruck (mehr) ist. constexpr sizet N = 5; sollte eigentlich funktionieren. Leider hab ich da auch keine schnelle Lösung parat, wie man sowas kurz und elegant handhabt - hab noch keinen constexpr-String implementiert.

    Genau, const char * ist kein constexpr Ausdruck. Hier muss auf jeden Fall mit constexpr Konstruktoren gearbeitet werden, sonst bricht man irgendwo die constexpr Kette auf und der Compiler beschwert sich. C-arrays sind an der Stelle eine ganz üble Geschichte, weswegen man ja extra die std::array Kapselung geschaffen hat.

    Warum? Geben die nicht nur eine Referenz auf ein Array-Element zurück?

    Und wenn man daneben greift gibt es nicht mal eine Exception sondern direkt ein std::terminate.



  • @VLSI_Akiko sagte in constexpr function Ergebnis in einer aufrufenden constexpr function als Template-Argument nutzen:

    Warum? Geben die nicht nur eine Referenz auf ein Array-Element zurück?

    Und wenn man daneben greift gibt es nicht mal eine Exception sondern direkt ein std::terminate.

    Das müsstest du mir nochmal genauer auseinandersetzen, vielleicht bin ich da schwer von Begriff. Klassiche C-Arrays werfen meines erachtens nicht, wenn man "daneben greift". Und alles, was beim Zugriff auf die zurückgegebene Referenz geworfen werden könnte, wird nicht aus der Operator-Funktion heraus geworfen, sondern von dort, wo er Zugriff stattfindet. Speziell solche "Durchreich-Funktionen" mache ich für gewöhnlich auch noexcept, daher interessieren mich die Gründe.

    Anders sähe es natürlich aus, wenn der Operator by value zurückgäbe - da kann natürlich der Konstruktor werfen - aber bei einer Referenz? ... oder wenn data ein beliebiges T wäre, das ein array-artiges Interface implementiert - dann würde ich auch auf noexcept verzichten oder mit noexcept-Operator arbeiten.



  • @Finnegan sagte in constexpr function Ergebnis in einer aufrufenden constexpr function als Template-Argument nutzen:

    Das müsstest du mir nochmal genauer auseinandersetzen, vielleicht bin ich da schwer von Begriff. Klassiche C-Arrays werfen meines erachtens nicht, wenn man "daneben greift". Und alles, was beim Zugriff auf die zurückgegebene Referenz geworfen werden könnte, wird nicht aus der Operator-Funktion heraus geworfen, sondern von dort, wo er Zugriff stattfindet. Speziell solche "Durchreich-Funktionen" mache ich für gewöhnlich auch noexcept, daher interessieren mich die Gründe.

    Hmm ja, da hast du recht. Allerdings sagt dir ein moderner Compiler auch gleich, dass da was fischig ist.

    <source>:14:22: warning: array subscript 10 is outside array bounds of 'array<int, 10> [1]' [-Warray-bounds]
       14 |     std::cout << a[10] << std::endl;
          |  
    


  • Also es ist machbar, aber diese C++ template Konstruktion mit C Funktionen wie printf kompatipel zu machen, ist eine ganze schöne Frickelei. Vor allem ist deine Version eines Pascal Strings eigentlich nicht korrekt, denn der intern gehaltende String endet dort nach wie vor auf \0.

    Aber im Prinzip könnte man es so machen, vielleicht gibt dir das ja ein paar Ideen:

    #include <cstdio>
    #include <type_traits>
    
    template <typename T>
    constexpr size_t length(const T &s)
    {
        return sizeof (s);
    }
    
    template <typename T, size_t N>
    class parray {
        using EType = std::remove_extent_t<T>;
        EType data[N + 1];
    public:
        constexpr parray() noexcept : data{N} {}
        constexpr EType &operator[](const size_t i) noexcept { return data[i + 1]; }
        constexpr const EType &operator[](const size_t i) const noexcept { return data[i + 1]; }
        constexpr size_t operator*() const noexcept { return data[0]; }
        constexpr const EType *operator+(const size_t pos) const { return data + pos; }
    };
    
    // Mache Pascal-String mit Längenbyte davor
    template <typename T>
    constexpr auto pstring(const T &s)
    {
        constexpr size_t N = sizeof (s);
        parray<T, N> PA;
        for (size_t i = 0; i < N; ++i)
            PA[i] = s[i];
        return PA;
    }
    
    constexpr auto len = length("Hallo");
    constexpr auto str = pstring("Hallo");
    
    int main()
    {
        printf("%ld, %s, %ld, %ld\n", *str, str + 1, len, sizeof (str));
    
        return 0;
    }
    

    Diese Variante wird aber nicht mit Laufzeitstrings Funktionieren, dass ist wirklich eine reine Compilezeit Implementierung. Und die Implementierung ist alles andere als sauber. Der Trick ist es mit Referencen zu arbeiten, denn die können im Vergleich zu char-Pointern wirklich compilezeit-konstant sein.



  • So nun ein Vorschlag von mir, das ganze lässt sich allerdings erst ab C++20 übersetzen. Man kann mit kleinen Änderungen size_t durch uint8_t ersetzen, und so die Pascal typische Speicherung nachbauen.

    #include <iostream>
      
    template <typename T>
    constexpr size_t length (const T* s) {
        return *s ? 1 + length (s + 1) : 0;
    }
    
    template <size_t s>
    class PString {
        constexpr static size_t size_ = s - 1;
        char data_[s - 1];
    public:
        constexpr PString () {
            for (size_t i = 0; i < s - 1; ++i) {
                data_[i] = '0';
            }
        }
        constexpr PString (const char (& str)[s]) {
            for (size_t i = 0; i < s - 1; ++i) {
                data_[i] = str[i];
            }
        }
        char& operator[] (const size_t i) {
            return data_[i];
        }
        const char& operator[] (const size_t i) const {
            return data_[i];
        }
        constexpr size_t size () const {
            return size_;
        }
        std::ostream& print(std::ostream& o) const {
            for (size_t i = 0; i < s - 1; ++i) {
                o << data_[i];
            }
    
            return o;
        }
    };
    
    template<size_t s>
    std::ostream&
    operator<< (std::ostream& o, const PString<s>& ps) {
        return ps.print(o);
    }
    
    //
    // Änderung
    //
    constexpr auto len = length("Hallo"); // geht
    const char str_raw[] = "Hallo";
    
    static constexpr char const* const p = "Hallo";
    
    constexpr PString<sizeof(str_raw)> str (str_raw);
    
    constexpr PString<length(p)+1> str2 (p);
    
    int main() {
        std::cout << sizeof(str_raw) << "," << str.size() << "," << str << "," << len << "\n";
        std::cout << length(p) << "," << str2.size() << "," << str2 << "," << len << "\n";
    }
    


  • Sorry fürs späte Antworten. An sich hab ich die Alamierung bei neuen Post für diesen Thread aktiviert, es kamen aber keine Mails ... Naja egal, jetzt hab ich die Antworten gesehen 🙂

    Ich geh mal die verschiedenen Fragen/Punkte durch:

    • Aktuell wird C++17 genutzt. es muss nicht C++11 sein.

    • Die stdio ist daher drinnen, weil der Kollegen, der das von mir wissen wollte, viel für Mikrocontroller programmiert und eher aus der C-Ecke kommt. Da ich mich mit constexpr-Funktionen bisher nicht großartig auseinandergesetzt habe, konnte ich die Frage nicht beantworten. Da es mich jetzt aber auch stark interessiert, wie man das lösen könnte, hab ich die Frage hier ins Forum gestellt.

    • "Vor allem ist deine Version eines Pascal Strings": Für das Zielsystem passt die Formatierung. Daher bitte nicht zu sehr auf das Kommentar "Pascal-String" festlegen.

    Die C++20-Lösung schau ich mir in jedem Fall mal im VS mal an. Ob der eigentliche Zielcompiler das kann, muss ich jetzt erstmal rausfinden.

    Vielen Dank an alle Poster für die Antworten!


Log in to reply