API Design: polymorphen return Type downcasten



  • Hallo Leute.

    Ich habe einen ServiceManager bei dem sich der User eigene Services registrieren kann und anhand der Service ID auch andere Service-Instanzen wieder holen kann:

    class ServiceManager {
    public:
      void registerService(string const& name, Service* service) {
        services[name] = service;
      }
    
      Service* getService(string const& name) {
        return services[name];
      }
    };
    

    Alle Services erben von "Service" - soweit klappt das ja auch. Das "Problem" an der Sache ist die Anwendung:

    MyService* s = static_cast<MyService*>(m.getService("myService"));
    

    bzw. eigentlich müsste es ja sogar ein dynamic_cast sein.

    Meine erste Idee war dann, in getServices selber zu casten

    template<typename ServiceT>
    ServiceT* getService(string const& name) {
      return static_cast<ServiceT*>(services[name]);
    }
    
    auto s = m.getService<MyService>("myService");
    

    das ist OK, aber gibt es vielleicht etwas besseres?
    Prinzipiell ist es so, dass sobald ein Service einmal registriert ist, der Typ auch fix feststeht. Das Registrieren findet beim Starten des Programmes statt und nachher wird nur noch per getService darauf zugegriffen.

    Deshalb meine Frage: gibt es hier irgendwelche coolen Tricks um diesen Downcast besser zu verstecken? Im idealfall will ich MyService gar nicht angeben wenn ich getService("myService") mache. "myService" muss auch kein String sein - es kann auch eine Compiletime Konstante sein.



  • Aus Client Sicht wären doch eigene Getter vielleicht ganz nett:

    MyService* s = m.getMyService("myService");
    

    Shade Of Mine schrieb:

    Im idealfall will ich MyService gar nicht angeben wenn ich getService("myService") mache.

    Wie soll das gehen? Funktionsüberladungen, die sich nur durch den Rückgabe-Wert unterscheiden, gibts ja nicht.



  • Shade Of Mine schrieb:

    "myService" muss auch kein String sein - es kann auch eine Compiletime Konstante sein.

    Also vielleicht

    #define GET_SERVICE(S) cast<S>(theServiceManager.getService(#S))
    …
    auto s=GET_SERVICE(myService);
    


  • Jockelx schrieb:

    Aus Client Sicht wären doch eigene Getter vielleicht ganz nett:

    MyService* s = m.getMyService("myService");
    

    Ja, nur wie bekomme ich die in ServiceManager rein?
    Klar kann man etwas wie:

    template<typename Next>
    struct ServiceManager : public Next {
    };
    
    template<typename Next>
    struct MyServiceCreator : public Next {
      MyService* getMyService() {...}
    };
    
    typedef ServiceManager<MyServiceCreator<MyOtherServiceCreator<Done>>> RealServiceManager;
    

    machen, aber das ist dann doch etwas unpraktisch.

    Shade Of Mine schrieb:

    Im idealfall will ich MyService gar nicht angeben wenn ich getService("myService") mache.

    Wie soll das gehen? Funktionsüberladungen, die sich nur durch den Rückgabe-Wert unterscheiden, gibts ja nicht.

    Klar geht das. Mit einem Proxy Objekt und einem operator T().
    Aber dann muss ich ja wieder MyService angeben - und zwar beim Typen von s und kann dort nicht auto schreiben.

    Ich hatte gehofft dass es vielleicht eine Trick per typeid oder derartiges gibt. Im Prinzip reicht mir meine template-Funktion Loesung ja. Sie ist OK. Aber vielleicht hat ja Jemand irgendwann schonmal eine perfekte Loesung fuer das Problem gefunden 🙂

    @volkard:
    Danke aber Makros will ich hierfuehr lieber nicht einsetzen.



  • Naja, aus dem String kannst du den Typ nicht rausbekommen - zumindest nicht ohne gröbere metaprogramming Akrobatik. (Und selbst dann bräuchtest du einen Mapping-Tablem, der dann wieder die Service-Namen + dazugehörige Typen kennt.)

    Was allerdings einfach ginge wäre ein template-Overload der statt eines Strings ein "service name" Objekt nimmt. Und aus dem Typ dieses Objekts könntest du dann den Typ herleiten.

    static ServiceName<FooService, "Foo"> FooServiceName;
    
    ...
    
        auto svc = svcman.GetService(FooServiceName);
    

    Das hätte auch den Vorteil dass du dich beim Service-Namen nicht mehr vertippen kannst (bzw. nur mehr an einer Stelle).



  • Danke hustbaer. Das ist ein interessanter Ansatz.
    Wie gesagt der Service string ist frei änderbar, solche globalen Objekte wären da interessant.

    Einzig, wie würde ich dann am besten einen neuen Service registrieren?



  • Shade Of Mine schrieb:

    Deshalb meine Frage: gibt es hier irgendwelche coolen Tricks um diesen Downcast besser zu verstecken? Im idealfall will ich MyService gar nicht angeben wenn ich getService("myService") mache. "myService" muss auch kein String sein - es kann auch eine Compiletime Konstante sein.

    Auch auf die Gefahr hin von den Pros ausgelacht zu werden 😃

    Aber wenn "myService" auch eine Compilezeit Konstante sein kann, dann wird ja auch gar nichts dynamisch zur Laufzeit verändert, oder? Was spricht dann gegen:

    #include <map>
    #include <string>
    #include <iostream>
    
    struct service {};
    
    struct my_service_1 : service
    {
        struct First { static constexpr const char *ID = "my_svc_11"; using Type = my_service_1; };
        struct Second { static constexpr const char *ID = "my_svc_12"; using Type = my_service_1; };
        struct Third { static constexpr const char *ID = "my_svc_13"; using Type = my_service_1; };
    
        void foo(){ std::cout << "my_service_1::foo()\n"; }
    };
    struct my_service_2 : service  { /* etc */ };
    struct my_service_3 : service  { /* etc */ };
    
    struct service_manager
    {
        std::map<std::string, service*> m_services;
    
        template <typename service_id>
        void register_service()
        {
            using T = typename service_id::Type;
            m_services.insert(std::make_pair(service_id::ID, new T()));
        }
    
        template <typename service_id>
        typename service_id::Type *get_service()
        {
            return static_cast<typename service_id::Type*>(m_services.find(service_id::ID)->second);
        }
    };
    
    int main()
    {
        service_manager sm;
        sm.register_service<my_service_1::First>(); // Registrieren
        auto s = sm.get_service<my_service_1::First>(); // Laden
        s->foo(); // Benutzen
    }
    

Log in to reply