[Gelöst] Compiler verwendet Templatefunktion nicht?



  • Ich suche nach einer Möglichkeit, einen seriellen Output zur Laufzeit auf mehrere Kanäle zu duplizieren. Entwicklungsumgebung ist PlatformIO, C++11 und ein Espressif ESP32 mit Arduino-Core als Zielsystem.

    Minimal funktionaler Code zur Erläuterung:

    
    #include <Arduino.h>
    
    class AltLog : public Print {
    public:
      size_t write(uint8_t c) {
        Serial.printf("/1: %c", c);
        Serial.printf("/2: %c", c);
        return 5;
      }
    
      template <typename... Args>
      size_t printf(const char *format, Args&&... args) {
        size_t len = 0;
        char fmt[strlen(format) + 4];
        strcpy(fmt, "/1: ");
        strcat(fmt, format);
        len = Serial.printf(fmt, std::forward<Args>(args) ...);
        strcpy(fmt, "/2: ");
        strcat(fmt, format);
        len = Serial.printf(fmt, std::forward<Args>(args) ...);
        return len;
      }
    };
    
    AltLog a;
    
    Print *device = &Serial;
    
    void setup() {
      Serial.begin(115200);
      Serial.println("");
      Serial.println("_OK_");
      delay(5000);
    
      device->printf("Serial.\n");
    
      device = &a;
    
      device->printf("Alternate.\n");
    
    }
    
    void loop() {
    }
    

    AltLog macht nichts anderes, als die Ausgabe zweimal auf Serial auszugeben - ist eben nur als Erklärung gedacht.

    Die Ausgabe des obigen Programms ist

    
    _OK_
    Serial.
    /1: A/2: A/1: l/2: l/1: t/2: t/1: e/2: e/1: r/2: r/1: n/2: n/1: a/2: a/1: t/2: t/1: e/2: e/1: ./2: ./1: 
    /2:
    

    Es funktioniert zwar, aber man sieht, dass bei der Verwendung von AltLog die write()-Funktion benutzt wird, obwohl über die Templatefunktion sehr wohl printf() verfügbar wäre.

    Warum passiert das? Und kann ich das ändern?

    Hintergrund: write() ist sehr ineffizient, wenn die Ausgabekanäle z.B. TCP-Verbindungen sind, weil da für jedes Zeichen an jeden Client ein Paket geschickt würde.


  • Mod

    Der obige Code ist kein minimales, reproduzierendes Beispiel deines Problems. Ich kann nicht nachvollziehen, was genau zwischen printf und write differenziert. Oder wie das geschehen soll. (Arduino ist eine C-Bibliothek, wie soll diese indirekt ein Funktionstemplate aufrufen?)

    Vielleicht solltest Du eher direkt im Arduino Forum fragen, oder mal in den Header eintauchen und die relevante Maschinerie posten?



  • @Miq sagte in Compiler verwendet Templatefunktion nicht?:

    Print *device = &Serial;

    Du zeigst und nicht, wie Print definiert ist.

    Es funktioniert zwar, aber man sieht, dass bei der Verwendung von AltLog die write()-Funktion benutzt wird, obwohl über die Templatefunktion sehr wohl printf() verfügbar wäre.

    Das sieht man dort nicht. Man sieht nur, dass mit device->printf("Alternate.\n"); doch offenbar printf aufgerufen wird - das von Print.

    Versuchst du, virtuelle getemplatete Funktionen zu haben?

    Ah, Print scheint die Klasse hier zu sein: https://github.com/esp8266/Arduino/blob/master/cores/esp8266/Print.h mit size_t printf(const char * format, ...) __attribute__ ((format (printf, 2, 3)));

    Du hast ja einen Pointer auf Print, nicht auf AltLog. Das ->printf ist immer das aus der Print-Klasse. (falls das deine Frage war)



  • @Columbo sagte in Compiler verwendet Templatefunktion nicht?:

    Der obige Code ist kein minimales, reproduzierendes Beispiel deines Problems. Ich kann nicht nachvollziehen, was genau zwischen printf und write differenziert. Oder wie das geschehen soll. (Arduino ist eine C-Bibliothek, wie soll diese indirekt ein Funktionstemplate aufrufen?)

    Die Erwähnung von Arduino sollte nur die Verwendung von Serial erklären - Serial ist eine Instanz von HardwareSerial, die wiederum von Print abgeleitet ist.

    Print ist eine abstrakte Basisklasse, die zwingend die Implementierung von write() erfordert (write ist in Print virtuell), definiert aber unzählige weitere print-Varianten, die in Print mit Hilfe von write implementiert sind.

    Die Templatefunktion der abgeleiteten Klasse AltLog sollte eigentlich - so mein Verständnis - die Funktion gleicher Signatur in der Basisklasse überladen.

    Die Anwendung des Funktionstemplate mit der von Print vorgegebenen Signatur für printf ist IMHO eine reine Compilersache und hat nichts mit der semantischen Bedeutung der Klassen zu tun. Ich denke, ich könnte das Ganze auch völlig abstrakt mit eigenen Klassen neu bauen - dann wird der Code aber entsprechend länger, fürchte ich.

    Meine - unbestätigte - Vermutung bisher ist, dass durch den als Print *device; definierten Pointer die Funktion der Basisklasse Print aufgerufen wird und nicht die der abgeleiteten Klasse AltLog. Da write in Print virtuell ist, wird im Endeffekt dann AltLog.write() aufgerufen.

    Vielleicht solltest Du eher direkt im Arduino Forum fragen, oder mal in den Header eintauchen und die relevante Maschinerie posten?

    Das mit den Arduinoforen habe ich bereits versucht - da war der Tenor so ungefähr: "Wenn es nicht geht, dann mach' es nicht" - ich möchte aber verstehen, warum der Compiler hier so entscheidet.



  • @wob sagte in Compiler verwendet Templatefunktion nicht?:

    @Miq sagte in Compiler verwendet Templatefunktion nicht?:

    Print *device = &Serial;

    Du zeigst und nicht, wie Print definiert ist.

    Es funktioniert zwar, aber man sieht, dass bei der Verwendung von AltLog die write()-Funktion benutzt wird, obwohl über die Templatefunktion sehr wohl printf() verfügbar wäre.

    Das sieht man dort nicht. Man sieht nur, dass mit device->printf("Alternate.\n"); doch offenbar printf aufgerufen wird - das von Print.

    Versuchst du, virtuelle getemplatete Funktionen zu haben?

    Ah, Print scheint die Klasse hier zu sein: https://github.com/esp8266/Arduino/blob/master/cores/esp8266/Print.h mit size_t printf(const char * format, ...) __attribute__ ((format (printf, 2, 3)));

    Du hast ja einen Pointer auf Print, nicht auf AltLog. Das ->printf ist immer das aus der Print-Klasse. (falls das deine Frage war)

    Das hat sich gekreuzt... Ja, genau das ist meine Vermutung. Wenn tatsächlich Print.printf() statt AltLog.printf()aufgerufen wird: wie kann ich (sauber) den Aufruf der überladenen Funktion erzwingen? AltLog *device; geht ja nicht, weil dann device = &Serial; nicht mehr akzeptiert wird.



  • Was natürlich geht, ist

    dynamic_cast<AltLog *>(device)->printf("Alternate.%c\n", 'A');
    
    • aber dann müsste ich zur Compilezeit wissen, auf welchen Typ device zur Laufzeit zeigt.

    Das ließe sich eventuell mit einer hässlichen Präprozessorkrücke einbauen, aber 😱



  • Innerhalb von AltLog::printf wird serial.printf aufgerufen, genau wie bei AltLog::write.



  • @Tyrdal sagte in Compiler verwendet Templatefunktion nicht?:

    Innerhalb von AltLog::printf wird serial.printf aufgerufen, genau wie bei AltLog::write.

    "AltLog macht nichts anderes, als die Ausgabe zweimal auf Serial auszugeben - ist eben nur als Erklärung gedacht."



  • Für meinen speziellen Fall habe ich eine Lösung gefunden.

    Im Arduino/ESP32-Core ist in der Print-Klasse auch printf() implementiert. Dieses verwendet intern eine Funktion size_t write(const uint8_t *buffer, size_t size) für die Ausgabe der formatierten Zeichenkette.

    Die interne Implementierung dieser write()-Variante ruft dann für jedes Zeichen die "pure virtual" Funktion size_t write(uint8_t c) auf, die jede von Print abgeleitete Klasse implementieren muss. Daher wird der Aufruf von device->printf() wie folgt ausgeführt:

    Print::printf() --> Print::write(buffer, size) --> AltLog::write(c)
    

    Damit ist klar, dass das AltLog::printf() sowieso ignoriert wird.

    Was mir entgangen war: die Funktion size_t Print::write(const uint8_t *buffer, size_t size) ist ebenfalls als virtual deklariert, so dass man sie problemlos überladen kann.

    Ich habe also AltLog::printf() durch AltLog::write() ersetzt, wodurch die Aufrufsequenz jetzt

    Print::printf() --> AltLog::write(buffer, size)
    

    lautet und die Zeichenkette in einem Rutsch ausgegeben werden kann.


Log in to reply