Variadic template funktioniert nicht wie gedacht



  • Hi,

    ich versuche gerade, mir ein variadisches (schreibt man das so im Deutschen?) Funktionstemplate zu bauen, das als Argumente übergebene Werte in einer bestimmten Form in ein Zielarray schreibt. Das sieht bisher so aus:

    // add() variant to copy a buffer into MM_data. Returns updated MM_index or 0
      uint16_t add(uint16_t count, uint8_t *arrayOfBytes);
    
    // Terminate args recursion
      uint16_t add() { return 0; }
    
      // add() - add a single data element MSB first to MM_data. Returns updated MM_index or 0
      template <class T> uint16_t add(T v) {
        uint16_t sz = sizeof(T);    // Size of value to be added
    
        // Will it fit?
        if (MM_data && sz <= (MM_len - MM_index)) {
          // Yes. Copy it MSB first
          while (sz) {
            sz--;
            MM_data[MM_index++] = (v >> (sz << 3)) & 0xFF;
          }
          // Return updated MM_index (logical length of message so far)
          return MM_index;
        }
        // No, will not fit - return 0
        return 0;
      }
    
    // First arg is uint8_t
      template <uint8_t, class... Args> uint16_t add(uint8_t v, Args... args) {
        return add(v) + add(args...);
      }
    
    // First arg is uint16_t
      template <uint16_t, class... Args> uint16_t add(uint16_t v, Args... args) {
        return add(v) + add(args...);
      }
    
    // First arg is uint32_t
      template <uint32_t, class... Args> uint16_t add(uint32_t v, Args... args) {
        return add(v) + add(args...);
      }
    
    • Die erste Variante ist der Sonderfall, bei der ein anderes Array angefügt wird (deswegen Länge/Pointer als Argumente).
    • Die zweite soll die Rekursion über die Argumente stoppen, wenn alle Argumente verbraucht sind.
    • Die dritte ist die eigentliche Wirkfunktion, die ein einzelnes Argument schreibt.

    Bis hierher funktioniert es auch.

    • Die drei letzten Varianten sollen eine Kette von Argumenten der Typen uint8_t, uint16_t und uint32_t abarbeiten. Wenn ich die nicht alle drei explizit angebe, gibt es Zweideutigkeiten mit der ersten Variante oben.

    Ich habe im Programm jetzt z.B. ein add(uint8_t, uint8_t, uint16_t, uint16_t); - natürlich mit Variablen des jeweiligen Typs anstelle der Typnamen. Das compiliert ohne Probleme, nur mault der Linker dann (die Klasse, in der add() verwendet wird, heißt TCPRequest. Sie erbt add() von einer Basisklasse, in deren Header der Code oben steht):

    no instance of overloaded function "TCPRequest::add" matches the argument list -- argument types are: (uint8_t, uint8_t, uint16_t, uint16_t) -- object type is: TCPRequest
    

    Wo liegt mein Denkfehler?



  • @Miq sagte in Variadic template funktioniert nicht wie gedacht:

    mault der Linker

    Wie kommst du darauf?



  • Willst du 1 und viele nicht lieber in verschiedene Namen trennen und das variadic template auch 1 Argument behandeln lassen?

    Also:

    template <class... Args> uint16_t add(Args... args) {
        return (add_impl(args) + ... + 0);
    }
    

    Dein add mit einem Argument in add_impl umbenennen. Die 0-Argument-Funktion kann dann ebenfalls wegfallen.



  • @manni66 sagte in Variadic template funktioniert nicht wie gedacht:

    @Miq sagte in Variadic template funktioniert nicht wie gedacht:

    mault der Linker

    Wie kommst du darauf?

    Compiler, klar. Sorry.



  • @Miq sagte in Variadic template funktioniert nicht wie gedacht:

    // First arg is uint8_t
      template <uint8_t, class... Args> uint16_t add(uint8_t v, Args... args) {
        return add(v) + add(args...);
      }
    

    Du hast hier einen namenlosen Non-Type Template-Parameter vom Typ uint8_t. Das müsste man eigentlich mit add<5, ...>() oder sowas instanzieren. Ist das wirklich, was du hier vorhast? Ich denke du willst hier wohl eher nur ein template <class... Args> haben. Dass der erste Parameter ein uint8_t ist, legst du ja hier schon über den ersten Funktionsparameter fest.



  • @wob sagte in Variadic template funktioniert nicht wie gedacht:

    Willst du 1 und viele nicht lieber in verschiedene Namen trennen und das variadic template auch 1 Argument behandeln lassen?

    Also:

    template <class... Args> uint16_t add(Args... args) {
        return (add_impl(args) + ...);
    }
    

    Dein add mit einem Argument in add_impl umbenennen.

    Okay, kann ich so machen.

    Das Problem besteht aber jetzt darin, dass die explizit überladene Version uint16_t add(uint16_t count, uint8_t *arrayOfBytes); nicht gefunden wird, sondern auch da ein Template angewendet werden soll. Das scheitert dann daran, dass die add() mit einem Argument nicht mit uint8_t * umgehen kann.



  • @Miq sagte in Variadic template funktioniert nicht wie gedacht:

    Das Problem besteht aber jetzt darin, dass die explizit überladene Version uint16_t add(uint16_t count, uint8_t *arrayOfBytes); nicht gefunden wird, sondern auch da ein Template angewendet werden soll. Das scheitert dann daran, dass die add() mit einem Argument nicht mit uint8_t * umgehen kann.

    Aber warum? Daran wollte ich doch nix ändern.

    Also vollständig:

    #include <stdint.h>
    
    uint16_t add(uint16_t count, uint8_t *arrayOfBytes) {
        return 42;
    }
    
    template <typename T> uint16_t add_impl(T v) {
        return v;
    }
    
    template <class... Args> uint16_t add(Args... args) {
            return (add_impl(args) + ... + 0);
    }
    
    int main() {
      // return add((uint8_t)1, 3, 5);
      // return add();
      return add(uint16_t(1234), (uint8_t*) nullptr);
    }
    


  • @Finnegan sagte in Variadic template funktioniert nicht wie gedacht:

    @Miq sagte in Variadic template funktioniert nicht wie gedacht:

    // First arg is uint8_t
      template <uint8_t, class... Args> uint16_t add(uint8_t v, Args... args) {
        return add(v) + add(args...);
      }
    

    Du hast hier einen namenlosen Non-Type Template-Parameter vom Typ uint8_t. Das müsste man eigentlich mit add<5, ...>() oder sowas instanzieren. Ist das wirklich, was du hier vorhast? Ich denke du willst hier wohl eher nur ein template <class... Args> haben. Dass der erste Parameter ein uint8_t ist, legst du ja hier schon über den ersten Funktionsparameter fest.

    Ja, da hast Du natürlich recht - hatte ich inzwischen auch bemerkt, den ersten Parameter entfernt, bekomme aber jetzt das Problem, dass der Compiler nicht die explizite Überladung uint16_t add(uint16_t count, uint8_t *arrayOfBytes); benutzen will, sondern auch da ein Template - was dann scheitert. 😟

    error: invalid operands of types 'unsigned char*' and 'int' to binary 'operator>>'
             MM_data[MM_index++] = (v >> (sz << 3)) & 0xFF;
    


  • @wob sagte in Variadic template funktioniert nicht wie gedacht:

    return (add_impl(args) + ... + 0);

    Das klappt so aber nicht, oder? Ich bin in C++11 unterwegs - kam das später?





  • @Miq sagte in Variadic template funktioniert nicht wie gedacht:

    @wob sagte in Variadic template funktioniert nicht wie gedacht:

    return (add_impl(args) + ... + 0);

    Das klappt so aber nicht, oder? Ich bin in C++11 unterwegs - kam das später?

    Ich glaube das ist eine C++17 Fold Expression.



  • @Finnegan sagte in Variadic template funktioniert nicht wie gedacht:

    @Miq sagte in Variadic template funktioniert nicht wie gedacht:

    @wob sagte in Variadic template funktioniert nicht wie gedacht:

    return (add_impl(args) + ... + 0);

    Das klappt so aber nicht, oder? Ich bin in C++11 unterwegs - kam das später?

    Ich glaube das ist eine C++17 Fold Expression.

    Okay, dann kann ich das nicht nutzen. Mein Zielsystem kann maximal C++11.



  • @Miq sagte in Variadic template funktioniert nicht wie gedacht:

    Ja, da hast Du natürlich recht - hatte ich inzwischen auch bemerkt, den ersten Parameter entfernt, bekomme aber jetzt das Problem, dass der Compiler nicht die explizite Überladung uint16_t add(uint16_t count, uint8_t *arrayOfBytes); benutzen will, sondern auch da ein Template - was dann scheitert. 😟

    error: invalid operands of types 'unsigned char*' and 'int' to binary 'operator>>'
             MM_data[MM_index++] = (v >> (sz << 3)) & 0xFF;
    

    Das ist vielleicht noch nicht die beste Lösung, aber du könntest die Template-Funktionen mit enable_if und is_integral/is_pointer gezielt aktivieren/deaktivieren, so dass der Pointer nicht mehr greift.

    Oder in C++20: template <std::integral... Args>.



  • @Finnegan
    Da komme ich nicht raus aus dem Loch:

    
    template <class T,
              class = typename std::enable_if<std::is_integral<T>::value>::type, class... Args>
      uint16_t add(T v, Args... args) {
        return add(v) + add(args...);
    

    führt auch zu

    ...: In instantiation of 'uint16_t xxx::add(T) [with T = unsigned char*; uint16_t = short unsigned int]':
    ...:   required from 'uint16_t xxx::add(T, Args ...) [with T = unsigned int; <template-parameter-1-2> = void; Args = {unsigned char*}; uint16_t 
    = short unsigned int]'
    ...:   required from here
    ...: error: invalid operands of types 'unsigned char*' and 'int' to binary 'operator>>'
             MM_data[MM_index++] = (v >> (sz << 3)) & 0xFF;
                                      ^
    


  • Kannst du es nicht so machen:

    #include <stdint.h>
    
    uint16_t add(uint16_t count, uint8_t *arrayOfBytes) {
        return 42;
    }
    
    template <typename T> uint16_t add_impl(T v) {
        return v;
    }
    
    uint16_t add() { return 0; }
    
    template <class T1, class... Args> uint16_t add(T1 head, Args... tail) {
        return add_impl(head) + add(tail...);
    }
    
    int main() {
      //return add((uint8_t)1, 3, 5);
       //return add();
       return add(uint16_t(1234), (uint8_t*) nullptr);
    }
    


  • @wob sagte in Variadic template funktioniert nicht wie gedacht:

    template <class T1, class... Args> uint16_t add(T1 head, Args... tail) {
    return add_impl(head) + add(tail...);
    }

    Nope, das läuft auf das gleiche Problem hinaus:

    ...: In instantiation of 'uint16_t xxx::add_impl(T) [with T = unsigned char*; uint16_t = short unsigned int]':
    ...:   recursively required from 'uint16_t xxx::add(T1, Args ...) [with T1 = unsigned char*; Args = {}; uint16_t = short unsigned int]'
    ...:   required from 'uint16_t xxx::add(T1, Args ...) [with T1 = unsigned int; Args = {unsigned char*}; uint16_t = short unsigned int]'
    ...:   required from here
    ...: error: invalid operands of types 'unsigned char*' and 'int' to binary 'operator>>'
             MM_data[MM_index++] = (v >> (sz << 3)) & 0xFF;
                                      ^
    

    Der Compiler erkennt die explizit überladene Funktion nicht.

    Ich werde mir jetzt so helfen, dass ich die überladene Funktion anders nenne, also keine Überladung mehr mache.



  • Das verstehe ich aber nicht, denn mein Code funktioniert doch?!
    Zur Sicherheit hier der Link: https://gcc.godbolt.org/z/ne3xz7 - wie du siehst, kompiliere ich das mit C++11 und es werden 0, 1 und 3 Parameter sowie deine Spezialfunktion mit Pointer aufgerufen.



  • @wob
    Da sind wir schon zwei - ich verstehe es auch nicht. Es ist klar, mit der rekursiven template-Funktion kann der Compiler den Sonderfall signaturmäßig abbilden - aber warum sollte er das tun, wenn es eine explizite Funktion bereits gibt?

    Liegt vielleicht an der Entwicklungsumgebung: PlatformIO für ESP32 (benutzt clang, soviel ich weiß).

    Ich lasse jedenfalls jetzt die Pfoten davon - danke Euch für Eure Hilfe! 👍



  • @Miq Du könntest konkreten Code zeigen. Hilft nicht nur Dir sondern auch anderen.



  • @Miq sagte in Variadic template funktioniert nicht wie gedacht:

    ...: error: invalid operands of types 'unsigned char*' and 'int' to binary 'operator>>'
             MM_data[MM_index++] = (v >> (sz << 3)) & 0xFF;
                                      ^
    

    Lustigerweise habe ich das Problem nicht beim Testen. Da nimmt er auch ohne enable_if im Pointer/count-Fall die entsprechende Funktion. MSVC warnt hier allerdings im Gegensatz zu GCC/Clang (selbst mit -Wall -Wextra) bei den add-Templates: warning C4717: 'add<>': recursive on all control paths, function will cause runtime stack overflow.

    Und tatsächlich, beim näherem Hinschauen:

    template <class... Args> uint16_t add(uint8_t v, Args... args) {
        return add(v) + add(args...);
    }
    

    Diese Funktion als add(v) ohne weitere Argumente aufgerufen, ruft sich links vom + nochmal selbst mit exakt den selben Argumenten auf. Hier habe ich es stattdessen mal mit einem schnellen Fix via std::enable_if<(sizeof...(Args) > 0)> probiert, was auch zu funktionieren scheint:

    https://ideone.com/i0ENy7

    Bleibt nur die Frage, ob dein Compiler das auch checkt. Der scheint da ja ein paar komische Dinge zu machen, wenn ich mir die Probleme mit deinem Code und dem Code @wob so ansehe.


Anmelden zum Antworten