Template member Funktion Index pro Instanz vergeben



  • @Mechanics
    Ich verstehe gerade nicht wie du meinst dass sowas überhaupt compile-time gehen sollte. Global, klar, kann man sich vorstellen. Kann man in C++ nicht umsetzen, aber grundsätzlich ist einfach zu verstehen wie es denn theoretisch gehen könnte.

    Aber pro Instanz?



  • @hustbaer sagte in Template member Funktion Index pro Instanz vergeben:

    Ich verstehe gerade nicht wie du meinst dass sowas überhaupt compile-time gehen sollte.

    Meine ich ja nicht. Ich sehe auch keine Lösung und gehe auch davon aus, dass es keine gibt, außer, ich stehe zufällig auf dem Schlauch.
    Ich hatte u.a. auch darauf gehofft, paar Ideen oder Ansätze zu sehen, die mich vielleicht zu einer halbwegs akzeptablen Lösung führen.

    Theoretisch - nicht in C++ - könnte ich mir das schon auch vorstellen, warum nicht? z.B. so ein instance_storage modifier, und der Compiler kümmert sich darum, hinter der Objektinstanz Speicher dafür bereitzustellen, und der konkreten Funktionsinstanz einen Zeiger zur Verfügung zu stellen, der mit this zusammenhängt.

    Das wäre natürlich auch nicht komplett compile-time, das ist schon klar. Aber etwas, bei dem halbwegs sichergestellt ist, dass da nicht alle Objektinstanzen auf den gleichen Speicher verweisen, und wo man "einfach" über paar Indirektionen an den Wert kommt, ohne Synchronisierung und Maps.



  • Aber das ist ein klassisches Map-Problem, wie soll das dann ohne Map gehen?



  • wie waere es mit folgendem ansatz?

    class type_index
    {
       public:
          type_index(std::size_t val = 0) noexcept : m_index(val)
          {
          }
    
          type_index(const type_index&) = delete;
          auto operator=(const type_index&) -> type_index& = delete;
    
          operator std::size_t(void) 
          {
             if(m_index == 0)
             {
                m_index = ++s_index;
             }
    
             return m_index;
          }
    
    
       private:
          inline static std::size_t s_index = 0;
          std::size_t m_index;
    }; /* class type_index */
    
    
    template<class T>
    class type_data
    {
       public:
          inline static type_index index;
    }; /* class type_data */
    
    ....
    std::cout << "int index: " << type_data<int>::index << std::endl;
       std::cout << "float index: " << type_data<float>::index << std::endl;
       std::cout << "double index: " << type_data<double>::index << std::endl;
       std::cout << "long index: " << type_data<long>::index << std::endl;
    
       std::cout << "int index: " << type_data<int>::index << std::endl;
       std::cout << "float index: " << type_data<float>::index << std::endl;
       std::cout << "double index: " << type_data<double>::index << std::endl;
       std::cout << "long index: " << type_data<long>::index << std::endl;
    

    ausgabe:

    int index: 1
    float index: 2
    double index: 3
    long index: 4
    int index: 1
    float index: 2
    double index: 3
    long index: 4
    


  • kleine aenderungen damit index mit 0 beginnt:

    class type_index
    {
       public:
          type_index(void) noexcept : m_index(s_index++)
          {
          }
    
          type_index(const type_index&) = delete;
          auto operator=(const type_index&) -> type_index& = delete;
    
          operator std::size_t(void) const
          {
             return m_index;
          }
    
       private:
          inline static std::size_t s_index = 0;
          const std::size_t m_index;
    }; /* class type_index */
    
    
    template<class T>
    class type_data
    {
       public:
          inline static type_index index;
    }; /* class type_data */
    
    ....
    
       std::cout << "float index: " << type_data<float>::index << std::endl;
       std::cout << "long index: " << type_data<long>::index << std::endl;
       std::cout << "double index: " << type_data<double>::index << std::endl;
       std::cout << "int index: " << type_data<int>::index << std::endl;
       std::cout << "std::string index: " << type_data<std::string>::index << std::endl;
    
       std::cout << "int index: " << type_data<int>::index << std::endl;
       std::cout << "float index: " << type_data<float>::index << std::endl;
       std::cout << "double index: " << type_data<double>::index << std::endl;
       std::cout << "long index: " << type_data<long>::index << std::endl;
       std::cout << "std::string index: " << type_data<std::string>::index << std::endl;
    

    ausgabe:

    float index: 0
    long index: 1
    double index: 2
    int index: 3
    std::string index: 4
    int index: 3
    float index: 0
    double index: 2
    long index: 1
    std::string index: 4
    


  • @Meep-Meep
    Also der Name type_index ist IMO etwas unglücklich gewählt: https://en.cppreference.com/w/cpp/types/type_index
    Davon abgesehen: der Teil wie man ohne Map global jedem verwendeten Typ eine eindeutige, ansteigende, "dichte" ID zuweisen kann ist schon klar. Zumindest mir, und da @Mechanics auch eher ein ziemlich Heller ist vermutlich auch ihm.

    Der Teil wo das ganze pro Objekt funktionieren soll ist das Problem.

    @Mechanics
    Das Problem ist dass der Compiler nicht wissen kann welche Memberfunktionen du auf welche Objekte aufrufst. Das kann in einfachen Fällen theoretisch mit statischer Analyse hinhauen, aber generische Lösung gibt's dafür keine. z.B. da das ganze vom Input und/oder Environment abhängen kann.
    Ala

    void doInt(Foo& f) {
        f.foo(42);
    }
    
    void doDouble(Foo& f) {
        f.foo(2.71828);
    }
    
    int main() {
        Foo a;
        Foo b;
        if (time() % 2 == 0)
            doInt(a); // 1
        else
            doInt(b); // 1
        doDouble(a); // ???
        doDouble(b); // ???
    }
    

    D.h. du brauchst zumindest eine map<int, int> pro Objekt (sinngemäss, natürlich muss es keine std::map sein, kann irgendwas sein mit dem man möglichst performant int auf int abbilden kann. Bzw. size_t auf size_t - was auch immer).

    Das einzige was ich mir noch vorstellen kann wäre die "map" noch etwas weiter zu optimieren. Wenn man mal globale, dichte Indizes hat, und compile-time weiss wie viele, kann man in jedes Objekt ein Fixed-Size Array + Counter reinmachen, und dann

    if (!localTypeIndex[typeIndex])
        localTypeIndex[typeIndex] = ++counter;
    

    machen. Ist aber konzeptionell immer noch ne map<int, int>. Und das "compile-time weiss wie viele" ist dummerweise auch ein Problem. Das geht nämlich in C++ nicht.

    Die beste Lösung die ich mir vorstellen kann wäre demnach -- zumindest für Fälle wo das Set an global damit verwendeten Typen klein genug ist -- wäre demnach ein std::vector oder boost::small_vector + Counter pro Instanz.



  • @Meep-Meep Danke. Ja, das wäre nicht so das Problem, das Problem ist wirklich, dass ich das pro Objekt haben wollte.

    Genau genommen eigentlich nicht mal pro Objekt, sondern pro "Modul" oder "Cpp". @hustbaer Ja, was ich gestern geschrieben hatte, war Quatsch, ist offensichtlich. Ich war zu fixiert darauf, dass mich eigentlich gar nichts interessiert. Der Verwendungsfall, den ich habe, ist ganz simpel. Ich habe eigentlich nur ein "Objekt", um das es mir geht, und es gibt auch keine Funktonen, an die es per Referenz übergeben wird, und was andere mit dieser Klasse außerhalb dieses Moduls machen interessiert mich auch nicht. Und die Reihenfolge der Aufrufe ist auch gleich.
    Ich kann halt nur nicht (und wills auch nicht), die Schnittstelle dieser Klasse zu einem Template ändern. Ich würde daher tendieren, ein template Hilfsobjekt mitzugeben, das alle möglichen Typen kennt.



  • Ich kann mir da grad nix drunter vorstellen. Kannst du das mal skizzieren wie das aussehen würde -- wenn es die Funktionalität gäbe nach der du suchst? Also mit dem Hilfsobjekt und dem "anderen" Objekt.



  • ich antworte mal auf die frage so wie ich sie verstanden habe.
    mit hilfe von der constexpr hash function für strings https://www.c-plusplus.net/forum/topic/348084/compile-time-string-hash/16 hab ich mal folgendes zusammen gestrickt (quick&dirty). bitte nicht auf die namensgebung achten. das waere jetzt fuer jeweils eine eigene instance mit eigener type-index-liste pro modul/cpp-file:

    module_type_indeces.hpp

    #pragma once
    
    #include <cinttypes>
    #include <stdexcept>
    
    namespace brabel
    {
    	// 32 bit FNV prime = 2^24 + 2^8 + 0x93
    	constexpr std::uint32_t fnv32_prime{ 0x01000193 };
    
    	// FNV-0 hash of "chongo <Landon Curt Noll> /\\../\\" represented in ASCII
    	constexpr std::uint32_t fnv32_basis{ 0x811c9dc5 };
    
    	constexpr auto module_hash(const char *str) -> std::uint32_t
    	{
    		if (!str)
    			throw std::invalid_argument{ "Argument str is a null pointer!" };
    
    		std::uint32_t hash{ fnv32_basis };
    
    		while (*str)
    			hash = (hash ^ *str++) * fnv32_prime;
    
    		return hash;
    	}
    
    
    template<std::uint32_t HASH>
    class type_index
    {
       public:
          type_index(void) noexcept : m_index(s_index++)
          {
          }
    
          type_index(const type_index&) = delete;
          auto operator=(const type_index&) -> type_index& = delete;
    
          operator std::size_t(void) const
          {
             return m_index;
          }
    
       private:
          inline static std::size_t s_index = 0;
          const std::size_t m_index;
    }; /* class type_index */
    
    
    template<class T, std::uint32_t HASH>
    class type_data
    {
       public:
          inline static type_index<HASH> index;
    }; /* class type_data */
    
    
    template<std::uint32_t HASH>
    class data_type_per_module
    {
       public:
          template<class T>
          auto try_register(const T&) noexcept -> void
          {
             type_data<T, HASH>::index;
          }
    
          template<class T>
          auto index(void) noexcept -> std::uint32_t
          {
             return type_data<T, HASH>::index;
          }
    }; /* class data_type_indeces */
    
    } /* namespace brabel */
    

    test1.hpp

    #pragma once
    
    #include "./module_type_indeces.hpp"
    
    auto call_test_func1(void) -> void;
    
    

    test1.cpp

    #include "./test1.hpp"
    #include <iostream>
    #include <string>
    
    auto call_test_func1(void) -> void
    {
       
       brabel::data_type_per_module<brabel::module_hash(__FILE__)> data_types;
       data_types.try_register<int>(1);
       data_types.try_register(12.34f);
       data_types.try_register(std::string(""));
       std::cout << "call_test_func1" << std::endl;
       std::cout << "int index: " << data_types.index<int>() << std::endl;
       std::cout << "float index: " << data_types.index<float>() << std::endl;
       std::cout << "std::string: " << data_types.index<std::string>() << std::endl;
       std::cout << std::endl;
    }
    

    test2.hpp

    #pragma once
    
    #include "./module_type_indeces.hpp"
    
    auto call_test_func2(void) -> void;
    
    

    test2.cpp

    #include "./test2.hpp"
    #include <iostream>
    
    auto call_test_func2(void) -> void
    {
       brabel::data_type_per_module<brabel::module_hash(__FILE__)> data_types;
       data_types.try_register(std::string(""));
       data_types.try_register(12.34f);
       data_types.try_register<int>(1);
       std::cout << "call_test_func2" << std::endl;
       std::cout << "int index: " << data_types.index<int>() << std::endl;
       std::cout << "float index: " << data_types.index<float>() << std::endl;
       std::cout << "std::string: " << data_types.index<std::string>() << std::endl;
       std::cout << std::endl;
    }
    
    

    main.cpp

    #include "./test1.hpp"
    #include "./test2.hpp"
    
    auto main(int, const char**) -> int
    {
       call_test_func1();
       call_test_func2();
    
       return 0;
    }
    

    ausgabe:

    call_test_func1
    int index: 0
    float index: 1
    std::string: 2
    
    call_test_func2
    int index: 2
    float index: 1
    std::string: 0
    


  • Sorry, anscheinend habe ich mich immer noch zu schwammig ausgedrückt.

    @Meep-Meep Nette Idee. Ich würde das aber eher nicht in die API einbauen. Ansonsten müsste ich noch genauer evaluieren, wie sich das ganze verhält.
    Was ich versucht habe zu beschreiben, war nicht, was ich haben will, sondern die Situation, die ich habe, und warum ich zu der fehlerhaften Annahme gekommen bin, das Problem wäre theoretisch lösbar. Weil ich eben nicht allgemein genug drüber nachgedacht hatte, sondern alles außer meinem konkreten Spezialfall ausgeblendet habe.
    Was jetzt nicht bedeutet, dass ich es für eine gute Idee halte, eine Lösung für genau den Spezialfall zu basteln. Die Klasse, um die es mir geht, ist allgemein verwendbar. Die Einschränkung pro Modul ist nur etwas, was in dem einen (aber wichtigen) Fall vorliegt, den ich betrachtet habe. Das bedeutet nicht, dass ein anderer nicht irgendwo MyClass c1, c2, c3 schreiben darf. Wir haben zu viel Code und zu viele Mitarbeiter für Hacks 😉 Ansonsten wäre deine Lösung wohl etwas, was tatsächlich auf mein Problem passen würde.

    @hustbar: Die Aussage mit dem Hilfsobjekt war jetzt wieder nicht auf das bezogen, was ich ursprünglich im Sinn hatte. Das ist dann quasi ein Workaround. Mehr oder weniger Pseudocode:

    //generic implementation
    template <typename T>
    void foo(T x) {}
    
    //typesafe, optimized overload
    template <typename T, typename Handler>
    void foo(T x, Handler& handler) {
        //execute some common code
        handler.handle<T>(x);
    }
    
    template<typename... Args>
    class MyClassHandler
    {
    public:
      //T has to be one of the types in Args
      template <typename T>
      void handle(T x);
    };
    
    MyClass c;
    MyClassHandler<int, double, std::string> handler;
    c.foo(5, handler);
    
    

    Damit würde MyClassHandler einen Teil der Funktionalität von MyClass übernehmen, und wenn man den benutzen will, muss man eben auch MyClassHandler zusätzlich "verwalten". Das macht die API etwas hässlicher, ist aber denke ich sicher genug, ändert nichts an der bisherigen Funktionalität, und wenn einem diese Optimierung wichtig ist, dann muss man eben noch ein Objekt halten.



  • @Mechanics
    Ich sehe jetzt noch nicht wo der Typ-Index dabei verwendet wird. Daher Frage an dich: was würde passieren wenn jmd. sowas macht:

    MyClass c;
    MyClassHandler<int, double, std::string> handler;
    c.foo(3.1415);
    c.foo(5, handler); // yay or nay?
    

    Würde das dann zu einem Problem führen?

    Falls ja wäre es vermutlich besser alles bis auf die foo Funktionen in eine eigene Klasse auszulagern, die dann nur eine fooImpl Funktion hat welche den Typ-Index bereits als Parameter erwartet. Auf diese Klasse kann man dann dünne Wrapper aufsetzen. 1x den generischen und dann den variadic-template-typ-eingeschränkten. Damit bei den Indexen nix durcheinanderkommt.

    Ober man macht nur die variadic-template-typ Version, erlaubt aber auch andere Typen. Dabei müsste man dann statisch dispatchen ob bekannter Typ oder unbekannter Typ. Die bekannten Typen haben dabei dann fixe Indexe, und die unbekannten werden wie gehabt dynamisch indiziert. Und fangen dann halt bei N+1 zu zählen an statt bei 1.

    Dann könnte man MyClass<> überall verwenden wo man die Optimierung nicht braucht, und MyClass<int, double, std::string> dort wo man die Optimierung schon braucht.



  • @hustbaer sagte in Template member Funktion Index pro Instanz vergeben:

    Ich sehe jetzt noch nicht wo der Typ-Index dabei verwendet wird.

    Was die Klasse überhaupt macht, habe ich ja auch nie hingeschrieben 😉

    @hustbaer sagte in Template member Funktion Index pro Instanz vergeben:

    Würde das dann zu einem Problem führen?

    Nee, das wäre kein Problem.
    Ich muss mir das aber später noch im Detail überlegen und dir Performance dann im Detail testen. Wenn ich das eh schon auseinander ziehe, kann ich dann evtl. auch gleich etwas umbauen, wenn sich das lohnt. Hab jetzt nur keine Zeit, mich intensiver damit zu befassen.
    Worum es grob ging, es werden eine Reihe von Funktonszeigern abgespeichert. Das geht auch völlig generisch, daher ist es egal, ob man dann den "Handler" mitgibt, oder nicht. Die wollte ich aber nicht verstreut und mehrfach speichern. Der Handler würde sie dann wahrscheinlich gar nicht mehr brauchen.
    Wenn man beides gemischt aufruft, dann wären die halt immer noch mehrfach abgespeichert, so wie bisher. Das wäre nicht tragisch.


Log in to reply