Datalayer - Template Typ speichern?



  • Hallo zusammen,

    ich suche jetzt schon seit Stunden nach einer funktionierenden Lösung, aber komme nicht darauf.

    Ich möchte eine Art Datenzugriffs-Schicht implementieren, d.h. für jeden benötigten Typen gibt es eine entsprechende "Query", welche die "Beschaffung" der Daten erledigt. Die Queries werden von einem Katalog (evtl. nicht die beste Bezeichnung, aber mir ist grad nix besseres eingefallen) verwaltet. Letztlich handelt es sich beim Katalog um eine Fabrik, aber anstatt von:

    fabrik.getItem(); // alles fest in der Fabrik gecodet
    fabrik.getScene();
    

    möchte ich mehr Flexibiliät für Erweiterungen (mehr Typen oder flexible Quelle), also so etwas:

    DatabaseItemQuery itemQuery(db); // beschafft Daten aus einer DB
    FileSceneQuery sceneQuery(filename); // beschafft Daten aus einer Datei
    FooQuery fooQuery; // weiterer Typ
    
    fabrik.registerQuery<Item>(itemQuery);
    fabrik.registerQuery<Scene>(sceneQuery);
    fabrik.registerQuery<Foo>(fooQuery);
    
    fabrik.get<Item>();
    fabrik.get<Scene>();
    fabrik.get<Foo>();
    

    Hier ist jetzt mein aktuell letzter Versuch.

    class IQuery
    {
        public:
            virtual ~IQuery() = default;
    };
    
    template <class T>
    class AbstractQuery : public IQuery
    {
        public:
            virtual std::unique_ptr<T> get() = 0;
    };
    
    class Catalog
    {
        public:
            template<class T>
            void registerQuery(IQuery query)
            {
                queries[typeid(T).name()] = query;
            }
    
            template<class T>
            std::unique_ptr<T> get()
            {
                if(queries.find(typeid(T).name()) != queries.end())
                        {
                            auto query = dynamic_cast<T&>(queries[typeid(T).name()]);
                            return query.get();
                        }
            }
        private:
            std::unordered_map<std::string, IQuery> queries;
    };
    
    //
    // Testklassen
    //
    
    class Scene
    {
        public:
            void say()
            {
                std::cout << "Scene!" << std::endl;
            }
    };
    
    class Item
    {
        public:
            void say()
            {
                std::cout << "Item!" << std::endl;
            }
    };
    
    class SceneQuery : public AbstractQuery<Scene>
    {
        public:
            std::unique_ptr<Scene> get()
            {
                return std::unique_ptr<Scene>(new Scene{});
            }
    };
    
    class ItemQuery : public AbstractQuery<Item>
    {
        public:
            std::unique_ptr<Item> get()
            {
                return std::unique_ptr<Item>(new Item{});
            }
    };
    
    // Main
    
    int main(int argc, char *argv[])
    {
    
        // Funktionier!
        SceneQuery sceneQuery{};
        ItemQuery itemQuery{};
    
        auto scene1 = sceneQuery.get();
        auto item1 = itemQuery.get();
    
        scene1->say();
        item1->say();
    
        // Wunschlösung
        Catalog catalog;
        catalog.registerQuery<Scene>(sceneQuery);
    
        auto scene2 = catalog.get<Scene>();
    
        scene2->say();
    }
    

    Er scheitert beim get() des Katalogs, weil im Katalog die Queries gespeichert sind, ich aber als Templatetypen ein Item zurückhaben möchte. In der Map kann ich nur identische Typen speichern (deshalb IQuery), damit muss ich irgendwie auf den richtigen Querytypen zurück casten. Aber woher nehmen?

    Vermutlich könnte so etwas funktionieren:

    catalog.get<Scene, SceneQuery>(); // Echt nicht schön :(
    

    Oder vielleicht:

    catalog.registerQuery<Scene, SceneQuery>(sceneQuery) // damit könnte ich leben
    

    Damit wäre alles beisammen: Rückgabetyp, Querytyp und passende Implementation. Aber wo speichere ich das alles?



  • hi

    wenn ich richtig vermute, was du suchst, dann schau dir mal die locales und locale::id an. die machen sowas aehnliches.

    Meep Meep



  • @Meep Meep: Danke erst mal, aber ich verstehe nicht ganz was du meinst. Da geht es doch um Lokalisierung im Sinne von unterschiedlichen Sprachen usw.

    Ich habe noch mal nachgelesen, unter anderem festgestellt das Wertetypen für Polymorphie nicht so toll sind, aber letztlich bin ich noch nicht weiter gekommen.

    class Catalog
    {
        public:
            template<class T>
            void registerQuery(std::unique_ptr<T> query)
            {
                queries[typeid(T).name()] = std::move(query);
            }
    
            template<class T>
            T* get()
            {
                if(queries.find(typeid(T).name()) != queries.end())
                        {
                            return dynamic_cast<T*>(queries[typeid(T).name()].get());
                        }
                return nullptr;
            }
        private:
            std::unordered_map<std::string, std::unique_ptr<IQuery>> queries;
    };
    
    // main
    
    int main(int argc, char *argv[])
    {
        Catalog catalog;
    
        catalog.registerQuery<SceneQuery>(std::unique_ptr<SceneQuery>(new SceneQuery{})); // Hier wird die konkrete Query bekannt gemacht.
    
        // irgendwelcher Code
    
        auto scene1 = catalog.get<SceneQuery>()->get(); // Nicht gut. Hier soll die konkrete Query nicht mehr notwendig sein
                                                        // und auch nicht der zusätzliche Aufruf von get()!!!
    
        scene1->say();
    }
    

    Funktioniert zwar, aber ist nicht das was ich will (siehe Kommentar bei main). Es scheitert daran, dass die map nur einen Typen (keinen Templatetypen) speichert kann, in diesem Fall IQuery. In IQuery kann ich get() nicht deklarieren, weil der Rückgabetyp über das Template vorgegeben wird. Im get() von Catalog erhalte ich nur ein IQuery zurück, dass ich erst auf die konkrete Query casten müsste.



  • Hi!

    Eine Möglichkeit der Query-Typen beim Aufruf von get() zu vermeiden, ist diesen in der Objektklasse zu hinterlegen:

    class Scene
    {
        ...
        using QueryType = SceneQuery;
    };
    
    class Item
    {
        ...
        using ItemType = ItemQuery;
    };
    

    Oder wenn du das vermeiden möchtest, kann man die Verknüpfung von Objektklasse und Queryklasse in eine separaten "Traits"-Klasse packen.
    Dazu hatte ich eben, bevor ich deine letzte Antwort gelesen hatte ein kurzes Beispiel erstellt:

    ...
    template <typename T>
    struct QueryTraits
    {
    };
    
    class Catalog
    {
    public:
        template<class T>
        void registerQuery(typename QueryTraits<T>::QueryType query)
        {
            queries[typeid(T).name()] = std::make_unique<QueryTraits<T>::QueryType>(query);
        }
    
        template<class T>
        std::unique_ptr<T> get()
        {
            std::unique_ptr<T> result;
            // Verwende Iterator, um doppelte Suche in unordered_map zu vermeiden 
            // (einmal via find() und einmal via []-Operator, das war etwas ineffizient).
            auto iter = queries.find(typeid(T).name());
            if(iter != queries.end())
            {
                // Dynamischer Cast nach QueryType für den angegebenen Typen.
                auto query = dynamic_cast<typename QueryTraits<T>::QueryType*>(iter->second.get());
                // Wenn in Map gespeicherte Query den korrekten Typen hat, dann Pointer zurückgeben.
                if (query != nullptr)
                    result = query->get();
            }
            // ... ansonsten nullptr zurückgeben (Default-Wert von unique_ptr)
            return result;
        }
    private:
        // Man muss in der Map mit Pointern arbeiten, da man ansonsten ein Problem bekommt,
        // das als "Object Slicing" bekannt ist: Nur der IQuery-Anteil der Objekte wird gepeichert,
        // und sie verlieren quasi ihren "Dynamischen Typen" (der abgeleitete Query Typ).
        std::unordered_map<std::string, std::unique_ptr<IQuery>> queries;
    };
    
    ...
    
    template <>
    struct QueryTraits<Scene>
    {
        using QueryType = SceneQuery;
    };
    
    template <>
    struct QueryTraits<Item>
    {
        using QueryType = ItemQuery;
    };
    
    ...
    
    int main(int argc, char *argv[])
    {
        // Wunschlösung
        Catalog catalog;
        catalog.registerQuery<Scene>(sceneQuery);
    
        auto scene2 = catalog.get<Scene>();
    
        scene2->say();
    }
    

    Wie gesagt, das " using QueryType = SceneQuery; " kann man auch in die Scene/Item-Klasse packen. Manchmal möchte man jedoch solche Dinge trennen.

    Hoffe das ist Inspiration genug, um dein Problem, zu lösen.

    Gruss,
    Finnegan



  • hier meine loesung.
    die klasse IQueryID::id macht im grunde das selbe wie std::locale::id.
    ich verwende sie hier um den IQuery* klassen eindeutige IDs automatisch zu
    vergeben. dadurch kann die catalog klasse zum registrieren einen vector
    verwenden und der zugriff auf das gewuenscht object kann mit dem operator[] realisiert werden.

    class IQueryID
    {
       public:
          class id
          {
             public:
                using value_type = unsigned int;
    
                id(value_type val = 0) : m_id(val) { }
                id(const id&) = delete;
                auto operator=(const id&) -> id& = delete;
    
                operator value_type()
                {
                   if(m_id == 0)
                   {
                      m_id = ++s_id;
                   }
                   return m_id;
                }
    
             private:
                value_type m_id;
                static value_type s_id;
          };
    };
    
    unsigned int IQueryID::id::s_id = 0;
    
    class IQuery
    {
       public:
          virtual auto show_message(const std::string &str) -> void = 0;
          virtual auto get_id(void) noexcept -> IQueryID::id::value_type = 0;
    };
    
    class catalog
    {
       public:
          auto register_query(IQuery *q) -> void
          {
             auto t_id = q->get_id();
             if(t_id > m_queries.size())
             {
                m_queries.resize(t_id, nullptr);
             }
             m_queries[t_id - 1] = q;
          } 
    
          template<class T>
          auto get(void) -> T*
          {
             return reinterpret_cast<T*>(m_queries[T::unique_id - 1]);
          }
    
       private:
          std::vector<IQuery*> m_queries;
    };
    
    class IQuery1 : public IQuery
    {
       public:
          static IQueryID::id unique_id;
          virtual auto show_message(const std::string &str) -> void
          {
             MessageBoxA(0, str.c_str(), "IQuery1", MB_OK);
          }
          virtual auto get_id(void) noexcept -> IQueryID::id::value_type
          {
             return static_cast<unsigned int>(unique_id);
          }
    };
    
    IQueryID::id IQuery1::unique_id;
    
    class IQuery2 : public IQuery
    {
       public:
          static IQueryID::id unique_id;
          virtual auto show_message(const std::string &str) -> void
          {
             MessageBoxA(0, str.c_str(), "IQuery2", MB_OK);
          }
          virtual auto get_id(void) noexcept -> IQueryID::id::value_type
          {
             return unique_id;
          }
    };
    
    IQueryID::id IQuery2::unique_id;
    
    class IQuery3 : public IQuery
    {
       public:
          static IQueryID::id unique_id;
          virtual auto show_message(const std::string &str) -> void
          {
             MessageBoxA(0, str.c_str(), "IQuery3", MB_OK);
          }
          virtual auto get_id(void) noexcept -> IQueryID::id::value_type
          {
             return unique_id;
          }
    };
    
    IQueryID::id IQuery3::unique_id;
    
    auto WINAPI WinMain(HINSTANCE, HINSTANCE, char* , int) -> int
    {
       catalog cat;
       cat.register_query(new IQuery1);
       cat.register_query(new IQuery2);
       cat.register_query(new IQuery3);
    
       cat.get<IQuery1>()->show_message("das ist ein test");
       cat.get<IQuery2>()->show_message("das ist ein test");
       cat.get<IQuery3>()->show_message("das ist ein test");
    
       return 0;
    }
    


  • Vielen Dank an Finnegan für das ausführliche Beispiel mit den Traits. Das war neu für mich.

    Vielen Dank auch an Meep Meep. Ich muss dein Beispiel erst mal durcharbeiten, um zu verstehen, was du da machst.

    Ziel der ganzen Spielerei ist ja die Entkoppelung, insofern möchte ich nicht innerhalb der Objektklassen Informationen zu konkreten Implementationen der zugehörigen Queries hinterlegen. Muss ja auch nicht sein. Die Trait-Spezialisierung kommt einfach in den Header der jeweiligen Query (denn der Katalog soll ja auch nichts über konkrete Implementierungen wissen. Ich habe das auch gleich mal umgesetzt.

    // catalog.h
    template <typename T>
    struct QueryTraits {};
    
    class Catalog
    {
        public:
            template<class T>
            void registerQuery(std::unique_ptr<IQuery> query)
            {
                queries[typeid(T).name()] = std::move(query);
            }
    
            template<class T>
            std::unique_ptr<T> get(const std::string& id)
            {
                auto found = queries.find(typeid(T).name());
                if(found != queries.end())
                {
                    if (auto query = dynamic_cast<typename QueryTraits<T>::QueryType*>(found->second.get()))
                    {
                        return query->get(id);
                    }
                }
    
                return nullptr;
            }
        private:
            std::unordered_map<std::string, std::unique_ptr<IQuery>> queries;
    };
    
    // z.B. itemquery.h
    class ItemQuery : public AbstractQuery<Item>
    {
        public:
            std::unique_ptr<Item> get(const std::string& id) override
            {
                return std::unique_ptr<Item>(new Item{});
            }
    };
    
    template <>
    struct QueryTraits<Item>
    {
        using QueryType = ItemQuery;
    };
    

    Beim Ausprobieren bin ich jetzt aber etwas überrascht worden 😮

    int main(int argc, char *argv[])
    {
        Catalog catalog;
    
        catalog.registerQuery<Scene>(std::unique_ptr<SceneQuery>(new SceneQuery));
    
        // irgendwelcher Code
    
        catalog.get<Scene>("dummy")->say();
        catalog.get<Item>("dummy")->say(); // WTF!!!
    }
    
    // Ausgabe:
    // Scene!
    // Item!
    

    Obwohl keine ItemQuery registriert ist, erhalte ich trotzdem eine Ausgabe auf der Konsole und das Programm beendet ohne Fehler. Funktioniert übrigens auch ohne eine SceneQuery zu registrieren. Oder sogar so:

    class Catalog
    {
        public:
            template<class T>
            void registerQuery(std::unique_ptr<IQuery> query)
            {
                queries[typeid(T).name()] = std::move(query);
            }
    
            template<class T>
            std::unique_ptr<T> get(const std::string& id)
            {
    //            auto found = queries.find(typeid(T).name());
    //            if(found != queries.end())
    //            {
    //                if (auto query = dynamic_cast<typename QueryTraits<T>::QueryType*>(found->second.get()))
    //                {
    //                    return query->get(id);
    //                }
    //            }
    
                std::cout << "WTF!" << std::endl;
                return nullptr;
            }
        private:
            std::unordered_map<std::string, std::unique_ptr<IQuery>> queries;
    };
    
    // Ausgabe:
    // WTF!
    // Scene!
    // WTF!
    // Item!
    

    😕 😕 😕



  • Offenbar funktioniert der Code soweit, denn wenn ich den Rückgabewert korrekt mit Prüfung auf nullptr weiter verarbeite, dann verhält sich alles wie erwartet.

    auto s1 = catalog.get<Scene>("dummy");
        auto i1 = catalog.get<Item>("dummy");
    
        if (s1) s1->say();
        if (i1) i1->say();
    

    Aber ich hätte bei Zugriff auf einen Nullptr irgendeine Fehlermeldung erwartet, anstatt tatsächlich die korrekte Ausgabe zu liefern. Wie kommt's?



  • Meep Meep schrieb:

    hier meine loesung.

    class IQueryID
    operator value_type() // was bewirkt das?
    {
        if(m_id == 0)
        {
            m_id = ++s_id;
        }
        return m_id;
    }
    
    // War mir neu, dass sowas geht. Hat das einen Vorteil?
    auto register_query(IQuery *q) -> void
    }
    

    Falls ich das alles einigermaßen verstanden habe, dann löst es nicht mein Problem, denn ich möchte ja mit get() nicht die Query als Rückgabewert sondern das Ergebnis der Query.

    Ich habe oben dennoch noch ein paar Fragen reingeschrieben, weil mir hier einige neue unbekannte Dinge begegnet sind. Insofern noch einmal Danke dafür.

    Danke und Gruß.
    temi



  • operator value_type() ist ein konvertierungsoperator.
    er castet das id_object nach value_type, wobei value_type ein alias fuer unsigned int ist



  • temi schrieb:

    Falls ich das alles einigermaßen verstanden habe, dann löst es nicht mein Problem, denn ich möchte ja mit get() nicht die Query als Rückgabewert sondern das Ergebnis der Query.
    temi

    ich versteh wohl nicht ganz wie du das meinst.

    temi schrieb:

    std::unique_ptr<T> get(const std::string& id)
            {
                auto found = queries.find(typeid(T).name());
                if(found != queries.end())
                {
                    if (auto query = dynamic_cast<typename QueryTraits<T>::QueryType*>(found->second.get()))
                    {
                        return query->get(id);
                    }
                }
     
                return nullptr;
            }
    

    wenn ich das richtig sehe, suchst du in get nach dem konkreten IQuery und rufst von dem IQuery-Objekt die get-methode auf in dem du als parameters den std::string(id) übergibst. die methode Catalog::get gibt das das ergebnis zurueck.
    bei meiner variante lass ich mir das objekt zurueck geben und ruf dann selber die get-methode von dem IQuery-Objekt auf. da wurde nur der aufruf nach aussen verlagert. du kannst das natuerlich auch in der Catalog::get methode machen.
    ich hab es extern gemacht weil ich den ruckgabewert nicht wusste und auch nicht weiß ob jedes IQuery Objekt immer den selben Typ zurueckgibt.



  • [quote="Meep Meep"]

    temi schrieb:

    temi schrieb:

    std::unique_ptr<T> get(const std::string& id)
            {
                auto found = queries.find(typeid(T).name());
                if(found != queries.end())
                {
                    if (auto query = dynamic_cast<typename QueryTraits<T>::QueryType*>(found->second.get()))
                    {
                        return query->get(id);
                    }
                }
     
                return nullptr;
            }
    

    wenn ich das richtig sehe, suchst du in get nach dem konkreten IQuery und rufst von dem IQuery-Objekt die get-methode auf in dem du als parameters den std::string(id) übergibst. die methode Catalog::get gibt das das ergebnis zurueck.
    bei meiner variante lass ich mir das objekt zurueck geben und ruf dann selber die get-methode von dem IQuery-Objekt auf. da wurde nur der aufruf nach aussen verlagert. du kannst das natuerlich auch in der Catalog::get methode machen.
    ich hab es extern gemacht weil ich den ruckgabewert nicht wusste und auch nicht weiß ob jedes IQuery Objekt immer den selben Typ zurueckgibt.

    Richtig! Die get-methode der Query sucht (irgendwo: Datei, DB, Internet) über eine id nach einem bestimmten Object. Eine ItemQuery sucht nach einem Item und liefert ein Item zurück. Eine SceneQuery sucht nach einer Scene und liefert diese zurück. Der Katalog ist nur ein Sammler für die einzelnen Queries. Leider kann allerdings auf dem IQuery kein get() aufgerufen werden, weil get() ja unterschiedliche Rückgabewerte liefert und ein Template nicht in der Map gespeichert werden kann. Deshalb wird IQuery auf die konkrete Query gecastet, deren get-Methode aufgerufen und der Rückgabewert T geliefert.
    Möglich wäre jetzt get<T, Q>() (Typ der Query), aber die Benutzer des Katalogs sollen die konkrete Query nicht kennen müssen.

    Ziel ist Entkoppelung der Daten(beschaffung) von der Logik und die einfache Erweiterbarkeit (neue Datenklasse und neue Query in den Katalog).

    Dein Beispiel war auch sehr lehrreich für mich. Falls mich noch jemand kurz über das oben aufgetretene Problem erhellen könnte, dann wäre das sehr nett.

    Gruß,
    temi



  • temi schrieb:

    Offenbar funktioniert der Code soweit, denn wenn ich den Rückgabewert korrekt mit Prüfung auf nullptr weiter verarbeite, dann verhält sich alles wie erwartet.

    auto s1 = catalog.get<Scene>("dummy");
        auto i1 = catalog.get<Item>("dummy");
    
        if (s1) s1->say();
        if (i1) i1->say();
    

    Aber ich hätte bei Zugriff auf einen Nullptr irgendeine Fehlermeldung erwartet, anstatt tatsächlich die korrekte Ausgabe zu liefern. Wie kommt's?

    du kannst memberfunktionen einer klasse problemlos aufrufen, auch wenn der zeiger auf 0 zeigt. du darfst in der methode nur nicht auf membervariablen zugreifen, dann schepperts.



  • Meep Meep schrieb:

    du kannst memberfunktionen einer klasse problemlos aufrufen, auch wenn der zeiger auf 0 zeigt. du darfst in der methode nur nicht auf membervariablen zugreifen, dann schepperts.

    Ah, gut zu wissen. Danke!


Anmelden zum Antworten