Unique ID zur Compile Zeit generieren



  • Hallo Leute,

    ich bin auf der Suche nach einer Möglichkeit zur Compile Zeit eine eindeutige ID für verschiedene Typen zu generieren. Folgenden Code habe ich bis jetzt:

    template<class... Args>
    struct mp_unique_id
    {
    private:
        static void dummy() { }
    
    public:
        static constexpr size_t value = (uint8_t*)&mp_unique_id::dummy - (uint8_t*)1;
    };
    
    using t_unique_id_1 = mp_unique_id<int, int   >;
    using t_unique_id_2 = mp_unique_id<int, double>;
    

    Das funktioniert auch soweit, wenn ich zur Laufzeit auf ::value zugreifen möchte. Ich kann es z.B. in der Console ausgeben. Wenn ich ::value aber als Parameter in ein Template stecken möchte, bricht der Compiler (g++ 6.3.0 20170406) mit einem Internal Error ab.
    Gibt es noch andere Möglichkeiten eine eindeutige ID zur Compile Zeit zu generieren. Die IDs müssen dabei nur eindeutig innerhalb der aktuellen Translation Unit sein (was anderes würde eh nicht gehen).
    Danke für die Hilfe 🙂

    MfG Bergmann.



  • Es gibt das __ COUNTER __ -Makro, das in der aktuellen TU zu eindeutigen Werten expandiert. Ist nicht Standard, wird aber vom MSVC, GCC und Clang unterstützt.
    Du kannst auch das _LINE_ Makro benutzen, ergibt aber nur für jede neue Zeile einen neuen Wert und kann durch #line überschrieben werden. Ich auch nen neuen Standardvorschlag unter cppreference.com gesehen, der __LINE__ , __FILE etc. durch was besseres ersetzt. Kann ich aber gerade nicht finden.



  • Danke für den Tipp, aber den kannte ich schon und das hilft mir hier leider nicht weiter, weil ich für jede Template Spezialisierung eine extra ID haben möchte. Die Macros würden aber immer die selbe ID im Template erzeugen.


  • Mod

    Techel schrieb:

    Es gibt das __ COUNTER __ -Makro, das in der aktuellen TU zu eindeutigen Werten expandiert. Ist nicht Standard, wird aber vom MSVC, GCC und Clang unterstützt.

    Der Präprozessor hat doch überhaupt keinen Zugang zu dem Konzept einer Template-Instantiierung. Wenn wir verschiedenen Template-Instantiierungen verschiedene Zahlen zuweisen möchten, muss TMP Code her.

    Und da dir TU-übergreifende Kompatibilität nicht wichtig ist:

    #include <utility>
    
    namespace detail {
      template <std::size_t N>
      struct    Rung { friend constexpr void rung(Rung<N>); };
      template <std::size_t N>
      struct AddRung { friend constexpr void rung(Rung<N>){} };
    
      template <std::size_t N, typename... Args>
      constexpr auto identify(bool) {
        (void)AddRung<N>{};
        return N;
      }
      template <std::size_t N, typename... Args,
                int = (rung(Rung<N>{}), 0)>
      constexpr auto identify(int) {
        return identify<N+1, Args...>(0);
      }
    }
    
    template <typename... Args>
    constexpr auto Identifier = detail::identify<0, Args...>(0);
    
    #include <iostream>
    int main() {
      std::cout << Identifier<int> << Identifier<float> << Identifier<double>;
    }
    

    Funktioniert sowohl mit GCC als auch Clang unter C++14. Ich würde jedoch etwas anderes vorschlagen, denn diese Technik soll verboten werden; cf. core issue 2118. Warum nimmst du nicht einfach die Adresse der statischen Memberfunktion als ID? Die kannst du auch an Templateparameter vom Typ void(*)() übergeben. Wozu brauchst du konkrete Ganzzahlen?



  • Autsch ist das übel! 😮
    Finde ich gut wenn das verboten wird. Statt dessen könnte man ja einfach nen standardkonformen Weg machen z.B. Adressen von statischen Membern in nen (u)intptr_t zu konvertieren.

    BTW: Weiss du vielleicht wieso das nicht erlaubt ist?


  • Mod

    hustbaer schrieb:

    BTW: Weiss du vielleicht wieso das nicht erlaubt ist?

    <a href= schrieb:

    Core issue 1384">CWG deemed that the complications of dealing with pointers whose tpes changed (pointer arithmetic and dereference could not be permitted on such pointers) outweighed the possible utility of relaxing the current restriction.

    Dass man Zeiger nicht in Zahlen konvertieren kann, ist einfach eine Folge des Verbots von reinterpret_cast . Ich sehe auch nicht wirklich irgendeinen handfesten Anreiz, so eine Asymmetrie einzuführen. Die meisten Experten, vermute ich, würden auf die Strategie mit void(*)() als Typ deiner IDs hinweisen.



  • Danke für den Code, ich probier das mal aus.

    Ich brauch eine Zahl, weil ich aus dieser Zahl einen String generieren möchte (zur Compile Zeit) und dazu brenötige ich arithmetische Operationen, die auf Zeigern nicht erlaubt sind 😕

    €: dein Code geht nur wenn ich eine Ausnahme für "non-template-friend" rund rum mach

    #pragma GCC diagnostic ignored "-Wnon-template-friend"
    


  • @Arcoth
    Ahh, ich doof. Mit weniger Kopfweh nochmal drüber nachdenken hilft.
    Der schwerwiegendste Grund hat meiner Einschätzung nach nix damit zu tun dass rumgecastete Zeiger nicht constexpr dereferenziert werden könnten, und daher reinterpret_cast halt generell verboten ist. Das liesse sich leicht fixen indem man nur die Richtung int -> pointer verbietet.

    Viel problematischer ist aber dass der Wert des Integers der dabei entstehen soll einfach nicht sinnvoll "compile time" ermittelt werden kann. Weil der im Prinzip erst beim Linken feststeht. Linken kann man aber erst wenn man weiss was das Programm überhaupt machen soll. Und das kann man ja sehr einfach von compile-time-konstanten Integerwerten abhängig machen.

    Was dann auch gleich die Antwort darauf ist:

    Arcoth schrieb:

    Ich sehe auch nicht wirklich irgendeinen handfesten Anreiz, so eine Asymmetrie einzuführen. Die meisten Experten, vermute ich, würden auf die Strategie mit void(*)() als Typ deiner IDs hinweisen.

    Naja mit dem void(*)() kannst du halt nix machen, ausser ihn als Template Parameter zu verwenden. Aber du kannst nicht drauf switchen, keinen Hashwert daraus ausrechnen - dergleichen Dinge. Und selbst wenn du ne if-Kaskade hinschreibst wir der Compiler diese nicht gut optimieren können, da er den Wert des Zeigers ja noch nicht kennt.

    Und dann frage ich mich wozu ein void(*)() überhaupt noch gut sein soll, wenn alles was man damit anstellt eh erst zur Laufzeit ausgewertet werden kann. Dann kann man vermutlich genau so gut std::type_index nehmen. Bzw. wenn man some_key<T> in Templates als Key braucht, kann man wohl genau so gut gleich nur T als Key nehmen.


  • Mod

    hustbaer schrieb:

    @Arcoth
    Ahh, ich doof. Mit weniger Kopfweh nochmal drüber nachdenken hilft.
    Der schwerwiegendste Grund hat meiner Einschätzung nach nix damit zu tun dass rumgecastete Zeiger nicht constexpr dereferenziert werden könnten, und daher reinterpret_cast halt generell verboten ist. Das liesse sich leicht fixen indem man nur die Richtung int -> pointer verbietet.

    SIcher, aber warum sollte man pointer -> int konkret erlauben? Die WG Ausschüsse ist grundsätzlich pessimistisch eingestellt, und führen nur die Parts ein, die für ein Feature wesentlich sind. Alles andere wird dann mit entsprechenden Papers erweitert. Das strukturiert den Gesamtprozess. Es gab bisher schlicht keine motion bzgl. dieser Konvertierung. Das ist aber hier nicht der Hauptgrund, der kommt hier:

    Viel problematischer ist aber dass der Wert des Integers der dabei entstehen soll einfach nicht sinnvoll "compile time" ermittelt werden kann. Weil der im Prinzip erst beim Linken feststeht.

    Du hast Recht, der Linker führt relocation durch. Es kann unter gewissen Architekturen auch relocation zur load time notwendig sein, weshalb einem Symbol allgemein vor der Ausführung des Programs keine sinnvolle Adresse zuweisen kann.
    Aber das ist gar nicht nötig, denn schon wenn ein Zeiger via (int*)(intptr_t)ptr ausgedrückt wird, zeigt er nicht auf das Objekt, und kann nicht verwendet werden, um darauf zuzugreifen. Nicht einmal Vergleiche von zwei identisch hergeleiteten Zeigerwerten sind definiert: Weil diese Zeiger nicht nachweislich auf das selbe Objekt zeigen. Wo liegt also das Problem, zur compile time einen zufälligen Wert zuzuweisen, der für Objekte innerhalb einer TU konsistent ist? Oder ist meine Argumentation oben einfach nicht mit der Praxis kompatibel? Es kann gut sein, dass ein als reachable deklariertes Objekt dann auch durch einen solchen Ausdruck angefasst werden darf. Alles in allem ist die de facto Lage IMO in Ordnung.

    Edit: Man kann Clang dazu überreden, einen solchen scheinbar zufälligen Wert zu erzeugen, indem man ausnutzt, dass die Zweige in einer conditional expression mit Bedingung __builtin_constant_p als constant expressions übernommen werden.

    Was den Nutzen von Zeigern zur compile zeit anbelangt: Der TE hat ja gerade ein variadisches Template. Der Zeiger würde dann eben als eine Art Hash von beliebig vielen Typen fungieren. Es stimmt aber, dass man damit so gut wie nichts anstellen kann. Die nächste Frage wäre, was der TE mit diesem Mechanismus zu lösen versucht. XY halt.



  • Arcoth schrieb:

    Nicht einmal Vergleiche von zwei identisch hergeleiteten Zeigerwerten sind definiert: Weil diese Zeiger nicht nachweislich auf das selbe Objekt zeigen.

    Dafuq?
    reinterpret_cast T* a -> uintptr_t i -> T* b ist soweit ich weiss sehrwohl definiert, und zwar so dass danach a == b gelten muss.
    Oder meinst du den Fall T* a -> uintptr_t i -> U* b mit T != U?

    Arcoth schrieb:

    Wo liegt also das Problem, zur compile time einen zufälligen Wert zuzuweisen, der für Objekte innerhalb einer TU konsistent ist? Oder ist meine Argumentation oben einfach nicht mit der Praxis kompatibel? Es kann gut sein, dass ein als reachable deklariertes Objekt dann auch durch einen solchen Ausdruck angefasst werden darf. Alles in allem ist die de facto Lage IMO in Ordnung.

    Angesichts der Tatsache dass der Aufwand es zu implementieren um mehrere Grössenordnungen zu hoch wäre, finde ich es auch OK. Was aber nicht heisst dass es nicht praktisch wäre 🙂

    Arcoth schrieb:

    Die nächste Frage wäre, was der TE mit diesem Mechanismus zu lösen versucht. XY halt.

    Richtig. Das wäre interessant.


  • Mod

    hustbaer schrieb:

    Arcoth schrieb:

    Nicht einmal Vergleiche von zwei identisch hergeleiteten Zeigerwerten sind definiert: Weil diese Zeiger nicht nachweislich auf das selbe Objekt zeigen.

    Dafuq?
    reinterpret_cast T* a -> uintptr_t i -> T* b ist soweit ich weiss sehrwohl definiert, und zwar so dass danach a == b gelten muss.

    Ja, das würde ich auch sagen. Ich bin halt von einer sehr wortwörtlichen Exegese ausgegangen, aber die wäre wahrscheinlich sowieso nicht kohärent (bspw. ist das alternative Resultat entschieden negativ, wo man eher unspezifiziertes Verhalten erwarten würde). Und wenn wir vorschreiben, dass obiges funktionieren soll, dann können wir auch keinen zufälligen Wert zuweisen, da nicht über mehrere TUs einheitlich machbar. Du hast Recht. Ich berufe mich allerdings auf meinen (gerade beendeten!) Kaffeeentzug nach der Weissheitszahn-OP, der mich meiner Konzentration beraubt hat :p



  • Arcoth schrieb:

    Du hast Recht. Ich berufe mich allerdings auf meinen (gerade beendeten!) Kaffeeentzug nach der Weissheitszahn-OP, der mich meiner Konzentration beraubt hat :p

    Ich glaube wenn wir zählen wer von uns beiden in den letzten Jahren bezüglich Standard öfter daneben gelegen hat ... würde ich ziemlich alt aussehen. 😉



  • ps: Das reinterpret_cast -Verbot verhindert u.A. auch dass man offsetof standardkonform compile-time-const implementiert. Wobei ich das als einen Spezialfall ansehe der damit gut abgedeckt ist dass der Standard vorschreibt dass es offsetof zu geben hat, und die Implementierung es halt irgendwie magisch bereitstellen muss.


  • Mod

    Selbiges gilt für addressof . Es gibt sogar einen Paper Sketch, der die Konvertierung zu Zeigern auf character types zu erlauben sucht: http://apolukhin.github.io/constexpr_algorithms/reinterpret.html



  • Arcoth schrieb:

    Die nächste Frage wäre, was der TE mit diesem Mechanismus zu lösen versucht.

    Ich bau mir eine kleine Bibliothek, mit der ich SQL Queries zur Compile Zeit aus Klassen generieren kann. Dafür brauche ich einen zufälligen Compile-Time-String, den ich als Namen für mein SQL Feld nutzen kann. (Weil ich unter Umständen die selben Feld Namen in unterschiedlichen Tabellen nutzen werde) Bsp:

    SELECT Table1.Fuu AS MyUniqueIdentifier1, Table2.Fuu AS MyUniqueIdentifier2 FROM ...;
    

  • Mod

    Und wofür muss der String eine Compilezeit Konstante sein?



  • Muss er dann vermutlich eh nicht.

    Das Szenario kann man dann ja schön lösen indem man die Adresse einer statischen Membervariable des Template nimmt, diese dann (ohne constexpr) in einen uintptr_t konvertiert, und dann runtime hex-codiert. (Bzw. einfach den stream insertion operator für void* verwendet.)


  • Mod

    hustbaer schrieb:

    Muss er dann vermutlich eh nicht.

    Das Szenario kann man dann ja schön lösen indem man die Adresse einer statischen Membervariable des Template nimmt, diese dann (ohne constexpr) in einen uintptr_t konvertiert, und dann runtime hex-codiert.

    Und wozu die Konvertierung nach uintptr_t ? Kannst doch direkt auf den Zeiger sprintf 'n?



  • Ja, richtig.
    Oder stream insertion operator für void* (siehe EDIT von mir eben gerade).
    Ist mir dann auch aufgefallen 🙂

    Bzw. wenn die generierten IDs/Strings nicht "stable" zwischen verschiedenen Programmausführungen sein müssen könnte man auch einfach einen Atomic-Counter verwenden um ein function-local-static zu initialisieren. Wobei die Member-Adressen Variante vermutlich so ziemlich universell besser ist.


  • Mod

    Wenn wir aber von Tabellennamen sprechen, könnte man doch gleich die demanglete Version von typeid(x).name() verwenden, oder? Man muss schauen, dass das auch einigermaßen eindeutig ist (am besten vollständig qualifiziert).


Anmelden zum Antworten