Check einer Konstanten auf Eindeutigkeit zur Compilezeit


  • Mod

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

    Danke schön, aber Deine Lösung braucht Exceptions, die ich nicht verfügbar habe.

    Exceptions sind unwesentlich. Du kannst ja auch einfach das Program mit einem error log beenden.

    Genau so was schwebte mir vor, ich weiß nur nicht, wie ich das codieren sollte.

    Das geht jetzt moeglicherweise ueber Dein Sprachverstaendnis hinaus, aber theoretisch ist das hier moeglich:

    namespace detail{
        template <uint8_t V> struct rack {
            friend constexpr bool f(rack<V>);
        };
        template <uint8_t V> struct writer {
            friend constexpr bool f(rack<V>) {return true;}
        };
        
        template <uint8_t V, bool = f(rack<V>())>
        constexpr bool check(rack<V>) {return true;}
        constexpr bool check(...) {return false;}
    }
    
    // Original class:
    template <uint8_t Value>
    class FC {
    public:
        // other members...
        template <uint8_t X = Value,
                  bool exists = detail::check(detail::rack<X>{}),
                  int = sizeof(detail::writer<X>)>
        FC() {
            static_assert(!exists, "You can only create one constant per translation unit");
        }
    };
    
    
    FC<0> x;
    FC<0> z; // Error
    

    Funktioniert sowohl mit Clang als auch GCC. Der Mechanismus wird nur einen einzigen Durchlauf von overload resolution erlauben, den jeder darauffolgende Versuch den Konstruktor zu instantiieren fuehrt zu einem Fehler. Das muesste also das wiederholte Definieren eines derartigen Objekts unterbinden.

    Disclaimer: Diese Technik ist uebrigens ein wenig schwarz und ggf. nicht vollkommen konform, und koennte in der Zukunft irgendwann nicht mehr funktionieren.

    Edit^2: Du musst offensichtlich noch den Kopier/Move Konstruktor als deleted markieren.



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

    Das geht jetzt moeglicherweise ueber Dein Sprachverstaendnis hinaus, aber theoretisch ist das hier moeglich:

    Stimmt. 😉

    Ich lerne aber gerne dazu und werde mich da mal hineinfressen.

    Herzlichen Dank!



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

    Edit^2: Du musst offensichtlich noch den Kopier/Move Konstruktor als deleted markieren.

    Das habe ich bewusst unterlassen, um die erzeugten Konstanten als Funktionsparameter benutzen zu können - so was würde doch sonst scheitern?

    const FC<12> A;
    
    void some_func(const FC f) {
      if (f == A) { ... }
    }
    
    ...
    
      const FC<66> B;
      some_func(B);
    
    


  • Und gleich noch eine Frage. Was macht

    int = sizeof(detail::writer<X>)
    

    in Deinem Vorschlag? Irgendwie fehlt mir da eine Variable nach dem int, und verwendet wird das ja eigentlich auch nicht?


  • Mod

    @Miq Das, was Du dort schreibst, kann sowieso nicht funktionieren, weil FC ein Template ist. Es ist moeglich, FC zu einer gewoehnlichen Klasse zu machen, und nur den Konstruktor zu einem Template, falls das eher Deiner Erwartung entspricht:

        template <uint8_t X,
                  bool exists = detail::check(detail::rack<X>{}),
                  int = sizeof(detail::writer<X>)>
        FC(initializer<X>) {
            static_assert(!exists, "You can only create one constant per translation unit");
        }
    

    Du wuerdest dann nicht vermeiden koennen, die Definition der Counter etwas zu verunstalten:

    const FC READ_FILE_RECORD  = initializer<0x14>{};
    

    Edit:

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

    Und gleich noch eine Frage. Was macht

    int = sizeof(detail::writer<X>)
    

    in Deinem Vorschlag? Irgendwie fehlt mir da eine Variable nach dem int, und verwendet wird das ja eigentlich auch nicht?

    Deshalb ja meine Warnung, dass der Mechanismus auf subtilen Nuancen von Template Instantiierung und Name Lookup etc. basiert. Die Instantiierung von writer führt zur Instantiierung der Definition von f, was letztlich den Zustand veraendert und uns erlaubt, den ersten vom zweiten Durchgang zu unterscheiden.



  • @Columbo
    Ah, natürlich! Es gibt ja keine Klasse FC, sondern nur FC<11>, FC<13> usw.

    Ich glaube, ich werde mich doch zu einer statischen Bitmap in der Klasse FC durchringen und zur Laufzeit einen Fehler ausgeben, wenn im Konstruktor festgestellt wird, dass der Konstantenwert da schon eingetragen ist.

    Ein Schwachpunkt ist sowieso, dass das Ganze von der Reihenfolge der Compilierung und Laufzeitinitialisierung abhängt. Ein Benutzer könnte es schaffen, einen der Library-internen Codes zu definieren, bevor der Initialisierungscode der Library-Konstanten gelaufen ist. Dann schmeißt die Library den Fehler...


  • Mod

    @Miq sagte in [Check einer Konstanten auf Eindeutigkeit zur Compilezeit]

    Ein Schwachpunkt ist sowieso, dass das Ganze von der Reihenfolge der Compilierung und Laufzeitinitialisierung abhängt. Ein Benutzer könnte es schaffen, einen der Library-internen Codes zu definieren, bevor der Initialisierungscode der Library-Konstanten gelaufen ist. Dann schmeißt die Library den Fehler...

    Das ist nicht möglich, wenn die Library die Klasse definiert.

    Die Lösung die ich präsentiert habe hängt nicht von der static initialization order ab, sondern von der Reihenfolge der Instantiierung, welche innerhalb einer Uebersetzungseinheit vollkommen deterministisch ist.

    Ich würde aber auch dazu raten, die Laufzeit Lösung zu wählen, da die andere Lösung eben einen gewissen Umstand generiert.



  • Mache ich. Danke nochmals! 🥇



  • @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.



  • @Miq
    Was von der Library vs. vom User vergebene IDs angeht, würde ich vorschlagen hier einfach zwei Nummernkreise zu verwenden.

    Wenn der User dann trotzdem eine ID der Library verwendet kannst du das natürlich nicht verhindern. Aber du kannst es zumindest so machen, dass es nicht unabsichtlich passieren kann.

    z.B. so

    #include <stdint.h>
    
    class ID {
    public:
        template <uint8_t N>
        static constexpr ID library() {
            static_assert(N >= 100);
            return N;
        }
    
        template <uint8_t N>
        static constexpr ID user() {
            static_assert(N < 100);
            return N;
        }
    
        constexpr uint8_t value() const { return m_id; }
    
    private:
        constexpr ID(uint8_t id) : m_id{id} {}
        uint8_t m_id;
    };
    
    constexpr auto LibId = ID::library<100>();   // OK
    constexpr auto UserId = ID::user<42>();      // OK
    
    constexpr auto BadLibId = ID::library<3>();  // kompiliert nicht
    constexpr auto BadUserId = ID::user<200>();  // kompiliert nicht
    

    Wenn gewünscht liesse sich die ID::library Funktion auch als freie Funktion in einen detail-namespace packen.



  • @hustbaer Danke schön, das ist für mich zumindest verständlicher. Bis ich auf die Etage von @Columbo komme, vergeht noch eine Weile...

    Ich probiere gerade einen anderen Ansatz: eine Templateklasse, die für jede benutzte uint8_t x eine Singletoninstanz FCT<x> erzeugt und von der eigentlichen FC-Klasse das Interface erbt. Und diese Singletons will ich in einer statischen std::map<uint8_t, FC> halten, so dass ich über das FC-Interface auf die verschiedenen FCTs zugreifen kann. Die eigentlichen Konstanten wären dann Referenzen auf diese Singletoninstanzen.

    Wenn ich da was hinbekomme, poste ich es hier mal zur Begutachtung.



  • Okay, es klappt schon halbwegs:

    #include <cstdio>
    #include <cstdint>
    #include <cstdlib>
    
    // FC: bildet die eigentliche Zugriffsfunktion auf fc ab
    // block() und der private Konstruktor sollen direkte Instanzen verhindern
    class FC {
    protected:
      const uint8_t fc;
      virtual void block() = 0;
      FC(uint8_t x) : fc(x) { }
    public:
      FC() = delete;
      FC& operator=(const FC& other) = delete;
      operator const uint8_t() const { return fc; }
    };
    
    // FCT<n> erzeugt eine Singletoninstanz, abgeleitet von FC
    template <uint8_t f>
    class FCT : public FC {
    private:
      FCT() : FC(f) {
        static_assert(f < 128, "FC must be in [1..127].");
      }
      void block() { }
    public:
      static FCT& instance() {
        static FCT Instance;
        return Instance;
      }
    };
    
    int main(int argc, char **argv) {
      FC& A = FCT<1>::instance();
      FC& B = FCT<2>::instance();
      FC& C = FCT<2>::instance();     // Doppelverwendung
    
      printf("A=%lX, B=%lX, C=%lX\n", (long unsigned int)(&A), (long unsigned int)(&B), (long unsigned int)(&C));
    
      return 0;
    }
    

    liefert als Ausgabe

    A=56126CF99020, B=56126CF99040, C=56126CF99040
    

    FCT<2> ist also tatsächlich nur einmal erzeugt worden. Dadurch kann man zwar mehrere unterschiedlich benannte Referenzen auf das gleiche Objekt erzeugen, aber das ist weniger schlimm für meinen Zweck.



  • Ich brauche nochmal etwas Unterstützung. Ich habe die Idee etwas erweitert und halte Pointer auf alle angelegten Singleton-Instanzen in einer statischen Map, um später auch mit dem numerischen Wert eine der Instanzen ansprechen zu können:

    #include <cstdio>
    #include <cstdint>
    #include <cstdlib>
    #include <unordered_map>
    
    using std::unordered_map;
    
    // 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
    public:
      FunctionCode() = delete;             // No empty constructor
      FunctionCode& operator=(const FunctionCode& other) = delete;  // No copy constructor
      explicit operator const 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() { }
    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: '%x'\n", i, FunctionCode::code(i));
      }
      return 0;
    }
    

    Worüber ich stolpere, ist Zeile 65, die der Compiler anmosert:

    fc.cpp: In function ‘int main(int, char**)’:
    fc.cpp:65:50: error: cannot allocate an object of abstract type ‘FunctionCode’
       65 |     printf("%d: '%x'\n", i, FunctionCode::code(i));
          |                                                  ^
    fc.cpp:10:7: note:   because the following virtual functions are pure within ‘FunctionCode’:
       10 | class FunctionCode {
          |       ^~~~~~~~~~~~
    fc.cpp:14:16: note: 	‘virtual void FunctionCode::block()’
       14 |   virtual void block() = 0;            // virtual function to prevent direct instances
          |                ^~~~~
    

    Es wird also nicht die verfügbare Methode FunctionCode::operator uint8_t() aufgerufen, sondern offensichtlich versucht, eine temporäre Instanz zu erzeugen.
    Wenn ich hingegen die Konversion explizit angebe, klappt es:

    ...
        printf("%d: '%x'\n", i, uint8_t(FunctionCode::code(i)));
    ...
    

    Warum?



  • 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.


Anmelden zum Antworten