Check einer Konstanten auf Eindeutigkeit zur Compilezeit



  • Deine Basisklasse hat keinen virtuellen Destruktor!

    PS:
    In deiner code-Funktion solltest du nicht auf *codes[0] im "nicht gefunden"-Fall zugreifen. Die Map könnte ja leer sein (UB).
    Besser wäre entweder einen Zeiger zurückzugeben (und dann nullptr) oder aber eine Exception zu werfen.
    Eine andere Alternative wäre noch die Referenz auf eine statische-Default-Instanz.



  • @Miq sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    Es wird also nicht die verfügbare Methode FunctionCode::operator uint8_t() aufgerufen

    Warum auch? Wegen des '%x' im printf?
    Ausserdem hast du den operator ja extra explicit gemacht, also so dass ein FunctionCode eben nicht still und heimlich zu uint8_t wird.


  • Mod

    @Jockelx sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    @Miq sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    Es wird also nicht die verfügbare Methode FunctionCode::operator uint8_t() aufgerufen

    Warum auch? Wegen des '%x' im printf?

    Um das mal etwas zu erläutern: printf ist als variadische Funktion der C-Standardbibliothek bekanntermassen type oblivious, d.h. die tatsächlichen statischen Typen der Argumente werden ignoriert/ und durch die format specifier vom User angegeben. Die Argumente werden zwar vor variadischen Aufrufen 'promoted', aber nicht konvertiert. Klassenobjekte haben in einem printf Aufruf demnach nie etwas verloren.

    Allerdings kontrollieren gegenwärtig die meisten Compiler während der Übersetzung die statischen Typen und format specifier auf Kompatibilität, und geben dir eine Warnung. Deshalb der universelle Hinweis, immer die Warnungen auf die höchste Stufe zu stellen.

    @hustbaer sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    @Columbo
    Das ist wirklich ziemlich übel, und auch ziemlich schwer zu durchschauen.

    Und IMO bringt es auch nicht wirklich viel, da der User trotzdem Fehler machen kann. Er kann ja immer noch in mehreren TUs die selben Werte vergeben.

    "bringt nicht viel" sehe ich anders, weil es immer noch einen signifikanten Teil der failure modes abdeckt; ausserdem sehe ich nicht, welche Methode in der Lage waere, diesen failure mode abzudecken?

    Letztlich kann auch nur der TE beurteilen, was ausreichend ist.



  • @Th69 sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    Deine Basisklasse hat keinen virtuellen Destruktor!

    Stimmt! Aua... Vergessen.

    PS:
    In deiner code-Funktion solltest du nicht auf *codes[0] im "nicht gefunden"-Fall zugreifen. Die Map könnte ja leer sein (UB).
    Besser wäre entweder einen Zeiger zurückzugeben (und dann nullptr) oder aber eine Exception zu werfen.
    Eine andere Alternative wäre noch die Referenz auf eine statische-Default-Instanz.

    Ein Zeiger passt nicht zu meiner Vorstellung einer spezialisierten Konstanten. Ich möchte dem Anwender das Konstrukt möglichst "enum-ähnlich" machen, aber eben mit der Möglichkeit, fallweise eigene Konstanten hinzuzufügen, die genauso verarbeitet werden wie die vorgegebenen.
    Exceptions haben auf dem Zielsystem (Embedded) wenig Sinn, sie würden die MCU in eine Bootschleife stürzen.
    Die default- Instanz ist der Wert 0, deswegen wird der in der cpp- Datei dazu vordefiniert. Aber nicht als statische, separate Instanz, so dass da eine minimale Chance besteht, auf unallokierten Speicher zuzugreifen. Muss mal sehen, wie ich das gestopft kriege.



  • Vielleicht noch mal ein paar Worte dazu, warum ich das eigentlich mache:

    ich habe eine Modbus-Library für ESP32 und ESP8266 entwickelt (Github: eModbus), die den Modbus-Standard ganz gut abdeckt. Die Funktionscodes des Modbus sind da ganz klassisch als enum vorgegeben.

    Es gibt aber inzwischen eine Reihe von Geräten, vor allem aus China, die sich nicht exakt an den Standard halten, sondern eigene Funktionscodes mitbringen - ob aus Kopierschutzgründen oder Unbedarftheit, weiß ich nicht.

    Um den Anwendern der Library die Möglichkeit zu geben, auch diese Nichtstandardfunktionen abzubilden, sollen sie in der Lage sein, diese im Userspace zu codieren. Ich möchte aber vermeiden, meinen Librarycode durch überall eingestreute Abfragen nach Sonderlocken aufzublähen, und diese zusätzlichen Funktionscodes einfach genauso behandeln wie die eingebauten, ohne dass die Library angefasst werden muss.



  • WTH? Ich gerate immer weiter in Schwierigkeiten, scheint mir. Ich wollte den virtuellen Destruktor einbauen:

    #include <cstdio>
    #include <cstdint>
    #include <cstdlib>
    #include <unordered_map>
    #include <iostream>
    #include <iomanip>
    
    using std::unordered_map;
    using std::cout;
    using std::endl;
    
    // FunctionCode: The interface to access the function code methods. 
    // block() and private constructor will prevent direct instances
    class FunctionCode {
    protected:
      const uint8_t fc;                    // The function code
      static unordered_map<uint8_t, FunctionCode*> codes; // Map of all codes so far
      virtual void block() = 0;            // virtual function to prevent direct instances
      FunctionCode(uint8_t x) : fc(x) { }  // Constructor accepting a uint8_t code
      virtual ~FunctionCode() = 0;                     // No destructor
    public:
      FunctionCode() = delete;             // No empty constructor
      FunctionCode& operator=(const FunctionCode& other) = delete;  // No copy constructor
      operator uint8_t() const { return fc; }  // Get function code as uint8_t
    
      // operator[uint8_t f] shall give access to a known code or 0 instead
      static const FunctionCode& code(uint8_t f) {
        unordered_map<uint8_t, FunctionCode*>::const_iterator got = codes.find(f);
        if (got == codes.end())
          return *codes[0];
        else
          return *got->second;
      }
    };
    
    // NewFC<n> will create a singleton instance derived from FunctionCode
    template <uint8_t f>
    class NewFC : public FunctionCode {
    private:
      // Private constructor will only be called internally
      NewFC() : FunctionCode::FunctionCode(f) {
        static_assert(f < 128, "FunctionCode must be in [1..127].");
        // Collect code in map
        codes[f] = this;
      }
      // Define virtual FunctionCode::block()
      void block() { }
      ~NewFC() override { }
    public:
      // Only method seen from outside is instance()
      // Will return (and in case create) a single instance of the given code
      static NewFC& instance() {
        static NewFC Instance;
        return Instance;
      }
    };
    
    // Define static map
    unordered_map<uint8_t, FunctionCode*> FunctionCode::codes;
    // Create default code 0x00
    FunctionCode& ANY_FUNCTION_CODE = NewFC<0>::instance();
    
    int main(int argc, char **argv) {
      FunctionCode& A = NewFC<1>::instance();
      FunctionCode& B = NewFC<2>::instance();
      FunctionCode& C = NewFC<2>::instance();  // Second use - shall return the same instance as B!
    
      printf("A=%lX, B=%lX, C=%lX\n", (long unsigned int)(&A), (long unsigned int)(&B), (long unsigned int)(&C));
    
      for (uint8_t i=0; i<5; ++i) {
        // printf("%d: '%u'\n", i, FunctionCode::code(i));
        cout << int(1) << ": '" << int(FunctionCode::code(i)) << "'" << endl;
      }
      return 0;
    }
    

    Da hat der Linker aber etwas gegen, und ich verstehe nicht, warum der Konstruktor von NewFC den Destruktor von FunctionCode aufrufen will:

    /usr/bin/ld: /tmp/ccPEOv4M.o: in function `NewFC<(unsigned char)0>::NewFC()':
    fc.cpp:(.text._ZN5NewFCILh0EEC2Ev[_ZN5NewFCILh0EEC5Ev]+0x7e): undefined reference to `FunctionCode::~FunctionCode()'
    /usr/bin/ld: /tmp/ccPEOv4M.o: in function `NewFC<(unsigned char)0>::~NewFC()':
    fc.cpp:(.text._ZN5NewFCILh0EED2Ev[_ZN5NewFCILh0EED5Ev]+0x26): undefined reference to `FunctionCode::~FunctionCode()'
    /usr/bin/ld: /tmp/ccPEOv4M.o: in function `NewFC<(unsigned char)1>::NewFC()':
    fc.cpp:(.text._ZN5NewFCILh1EEC2Ev[_ZN5NewFCILh1EEC5Ev]+0x7e): undefined reference to `FunctionCode::~FunctionCode()'
    /usr/bin/ld: /tmp/ccPEOv4M.o: in function `NewFC<(unsigned char)1>::~NewFC()':
    fc.cpp:(.text._ZN5NewFCILh1EED2Ev[_ZN5NewFCILh1EED5Ev]+0x26): undefined reference to `FunctionCode::~FunctionCode()'
    /usr/bin/ld: /tmp/ccPEOv4M.o: in function `NewFC<(unsigned char)2>::NewFC()':
    fc.cpp:(.text._ZN5NewFCILh2EEC2Ev[_ZN5NewFCILh2EEC5Ev]+0x7e): undefined reference to `FunctionCode::~FunctionCode()'
    /usr/bin/ld: /tmp/ccPEOv4M.o:fc.cpp:(.text._ZN5NewFCILh2EED2Ev[_ZN5NewFCILh2EED5Ev]+0x26): more undefined references to `FunctionCode::~FunctionCode()' follow
    collect2: error: ld returned 1 exit status
    


  • Virtual reicht, der muss nicht pure virtual sein. Ansonsten musst du den ja für jede abgeleitete Klasse implementieren.
    Aber implentieren musst du den auch.



  • @DocShoe sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    Virtual reicht, der muss nicht pure virtual sein. Ansonsten musst du den ja für jede abgeleitete Klasse implementieren.
    Aber implentieren musst du den auch.

    Das war es. Danke! 👍



  • Und der nächste Stolperstein... Ich bekomme das statische Default-Element nicht in der Klassendefinition hin.

    Ich benutze ja aktuell außerhalb der Klassendefinition so etwas:

    FunctionCode& noCode = NewFC<0>::instance();
    

    Das erzeugt also zuerst eine (Singleton-)Instanz der Klasse NewFC<0>, wobei intern der Konstruktor von FunctionCode aufgerufen wird und damit auch die Referenz auf die neu erzeugte Instanz in der statischen codes-Map abgelegt wird.

    Ich bekomme aber nicht in mein Hirn, wie dies innerhalb der Klassendefinition als statische Instanz erzeugt werden sollte, wenn die statische codes-Map noch überhaupt nicht initialisiert ist - auch das passiert ja erst extern.

    Und zweitens ist mir die Syntax unklar - ich würde in NewFC analog so etwas wie

    constexpr static FunctionCode& noCode = NewFC<0>::instance();
    

    vermuten, aber das frisst der Compiler nicht:

    fc.cpp:55:50: error: incomplete type ‘NewFC<0>’ used in nested name specifier
       55 |   constexpr static FunctionCode& noC = NewFC<0>::instance();
          |                                                  ^~~~~~~~
    

    Ich denke, da beißt sich die Katze sozusagen in den Schwanz, weil zur Erzeugung der statischen Instanz die Klasse als solches schon fertig definiert sein müsste, was ja wegen der fehlenden statischen Instanz nicht der Fall ist.



  • @Columbo sagte in [Check einer Konstanten auf Eindeutigkeit zur Compilezeit](/forum

    "bringt nicht viel" sehe ich anders, weil es immer noch einen signifikanten Teil der failure modes abdeckt; ausserdem sehe ich nicht, welche Methode in der Lage waere, diesen failure mode abzudecken?

    Fällt mir auch nix ein wie man das abdecken könnte.

    Ist mMn. aber auch Quatsch zu versuchen das zu lösen. Ich denke @Miq verrennt sich da gerade in etwas was vollkommen wurscht ist.



  • @hustbaer sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    Ist mMn. aber auch Quatsch zu versuchen das zu lösen. Ich denke @Miq verrennt sich da gerade in etwas was vollkommen wurscht ist.

    Mag ja sein. Ich habe vor ein paar Posts mal zu erklären versucht, warum ich mich daran abarbeite. Was daran ist Quatsch, was wurscht?



  • Selbst wenn man den Destruktor pure virtual macht (um zu verhindern, daß man direkt Objekte der Basisklasse erzeugen kann), so wird dennoch eine Definition benötigt:

    FunctionCode::~FunctionCode()
    {
    }
    

    damit dieser Destruktor beim Zerstören eines Objekts aufgerufen werden kann, s.a. Pure Virtual Destructor in C++.



  • @Th69 yessir, das hat @DocShoe ja auch geschrieben und so hat es dann auch funktioniertn



  • Nicht ganz, unsere Aussagen sind leicht unterschiedlich...


  • Mod

    @hustbaer sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    @Columbo sagte in [Check einer Konstanten auf Eindeutigkeit zur Compilezeit](/forum

    "bringt nicht viel" sehe ich anders, weil es immer noch einen signifikanten Teil der failure modes abdeckt; ausserdem sehe ich nicht, welche Methode in der Lage waere, diesen failure mode abzudecken?

    Fällt mir auch nix ein wie man das abdecken könnte.

    Mir schon, Du könntest ein non-inline external symbol in der TU definieren lassen, dessen Typ mit dem Code parametrisiert ist, was dann zu Linkerfehlern führt wenn mehrere TUs den gleichen Code verwenden (technisch ill-formed NDR aber praktisch immer diagnostiziert). Das funktioniert in meiner Lösung aber nicht, weil eine class-scope friend Definition die Funktion implizit inline macht. Ob das aber ohne ekligen boilerplate machbar ist...

    Edit: das ergibt eigentlich keinen Sinn, weil die meisten User ihre Codes doch in einem Header definieren wuerden. Du muesstest also eigentlich noch einen drauf setzen: wenn das selbe Code-Symbol in mehreren TUs definiert ist, ist es ok, aber wenn verschiedene Code-Symbole mit dem gleichen Typ ueber mehrere TUs definiert werden, dann sollen diese wiederum dasselbe non-inline Dummy-Symbol definieren, was dann zum Fehler fuehrt. Klingt aber knifflig umzusetzen.

    Um den Anwendern der Library die Möglichkeit zu geben, auch diese Nichtstandardfunktionen abzubilden, sollen sie in der Lage sein, diese im Userspace zu codieren. Ich möchte aber vermeiden, meinen Librarycode durch überall eingestreute Abfragen nach Sonderlocken aufzublähen, und diese zusätzlichen Funktionscodes einfach genauso behandeln wie die eingebauten, ohne dass die Library angefasst werden muss

    ... und warum genau ist es so entscheidend, dass der User keinesfalls einen Code zweimal benennen darf? Letztlich kann der User doch viele Dinge falsch machen. Man arbeitet unter der Annahme, dass er sorgfältig ist, und Codes zu definieren erfordert immer eine gewisse Akribie, weil man alles mit einer oder mehreren APIs abgleichen muss.



  • @Columbo sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    ... und warum genau ist es so entscheidend, dass der User keinesfalls einen Code zweimal benennen darf? Letztlich kann der User doch viele Dinge falsch machen. Man arbeitet unter der Annahme, dass er sorgfältig ist, und Codes zu definieren erfordert immer eine gewisse Akribie, weil man alles mit einer oder mehreren APIs abgleichen muss.

    Das kam dann falsch rüber - es ist völlig egal, wie die Codes benannt werden, es darf eben jeden nur genau einmal geben. Und die von der Library vorgegebenen sind schon da und können so nicht mit einer anderen Bedeutung versehen werden.
    Der Funktionscode hat nicht nur eine Nummer, sondern auch einen Typ und einen eigenen Satz von Parametern in Modbus-Nachrichten, die die Funktionen bedienen. Das ist in der Rumpfklasse meines Beispiels noch nicht enthalten, weil ich das Beispiel nicht unnötig aufblähen wollte. Deswegen muss sich FC13 immer wie FC13 darstellen, auch wenn er gerade Otto heißt.



  • @Miq sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    Der Funktionscode hat nicht nur eine Nummer, sondern auch einen Typ und einen eigenen Satz von Parametern in Modbus-Nachrichten, die die Funktionen bedienen. Das ist in der Rumpfklasse meines Beispiels noch nicht enthalten, weil ich das Beispiel nicht unnötig aufblähen wollte. Deswegen muss sich FC13 immer wie FC13 darstellen, auch wenn er gerade Otto heißt.

    Ich denke es wäre jetzt an der Zeit dass du mal ein Beispiel zeigst worum es wirklich geht.

    Klingt für mich nämlich danach als ob das alles leicht mit einem Klassentemplate lösbar sein sollte (keine Default-Implementierung, nur vollständige Spezialisierungen für die jeweiligen Function-Codes).


  • Mod

    @Miq sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    @Columbo sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    ... und warum genau ist es so entscheidend, dass der User keinesfalls einen Code zweimal benennen darf? Letztlich kann der User doch viele Dinge falsch machen. Man arbeitet unter der Annahme, dass er sorgfältig ist, und Codes zu definieren erfordert immer eine gewisse Akribie, weil man alles mit einer oder mehreren APIs abgleichen muss.

    Das kam dann falsch rüber - es ist völlig egal, wie die Codes benannt werden, es darf eben jeden nur genau einmal geben. Und die von der Library vorgegebenen sind schon da und können so nicht mit einer anderen Bedeutung versehen werden.

    Das ist doch einerlei. Das "Benennen" eines Codes sollte heissen, eine Variable zu definieren, die diesen Code repräsentiert. Ich meinte damit, er darf keine zwei Code-Proxies definieren, die den gleichen Code darstellen.



  • Für mich klingt das alles nach einem XY Problem...



  • @DocShoe sagte in Check einer Konstanten auf Eindeutigkeit zur Compilezeit:

    XY Problem

    Öh? Wenn ich nicht alle Details erwähnt habe, dann, um die Komplexität zu verringern, nicht, weil ich irgendwas verbergen wollte.


Anmelden zum Antworten