Signatur Klassen template



  • Hi zusammen,

    mir fällt grad kein passender Titel ein, wie ich das Problem nennen soll. Prinzipiell habe ich eine Variable in einer Klasse gekapselt, auf die policy-gesteuert zugegriffen werden kann. Es gibt zwei Policies: direkt ohne Zugriffsschutz und SynchronizedAccess mit einem Synchronisierungsobjekt, das den Zugriff aus mehreren Thread gleichzeitig verhindert.

    template<typename T>
    struct immediate_access
    {
       // direkter Zugriff ohne Synchroniserung: Wert darf als const-ref zurückgegeben werden
       using return_type = T const&;
    
       immediate_access() = default;
    
       return_type  value( T const& v ) const
       {
          return v;
       }
    
       void lock()
       {
       }
    
       void unlock()
       {
       }
    };
    
    template<typename T>
    struct synchronized_access
    {
       // in Multithreading-Umgebungen muss der Wert als Kopie zurückgegeben werden, außerdem wird ein Objekt für die Synchroniserung gebraucht.
       using return_type = T;
       sync_object Lock_;
       
       synchronized_access() = default;
    
       return_type  value( T const& value ) const
       {
          return value;
       }
    
       void lock()
       {
          Lock_.lock();   
       }
    
       void unlock()
       {
          Lock_.unlock();   
       }
    };
    
    // Default: Direkter Zugriff ohne Synchronisierung
    template<typename T, typename accessor = immediate_access<T>>
    class observable : public lockable
    {
       using value_type           = T;
       using const_reference      = T const&;
       using accessor_return_type = typename accessor::return_type;
    
       value_type   Value_;
       accessor     Accessor_;
    
    public:
       publisher    OnChange; // Pseudo Code für Callback-Handler
    
    public:
       observable() = default;
    
       accessor_return_type value() const
       {
          // RAII scoped Lock-Klasse, Zugriff wird über die Lebenszeit des lock-Objekts gesperrt
          scoped_lock const lock( *this );
          return Accessor_.value( Value_ );      
       }
    
       void set_value( const_reference val )
       {
          auto update = [&]()
          {
             scoped_lock const lock( *this );
             if( Value_ != val )
             {
                Value_ = val;
                return true;
             }
             return false;
          };
          if( update() )
          {
             // Subscriber über Wertänderung informieren
             OnChange( *this );
          }
       }
    
    private:
       void lock()
       {
          Accessor_.lock();
       }
    
       void unlock()
       {
          Accessor_.unlock();
       }
    };
    

    In der Anwendung soll das dann so aussehen:

    struct test
    {
       observable<int> o1; // Direkter Zugriff ohne Synchronisierung (default template Parameter ist immediate_access)
       observable<int,synchronized_access<int>> o2; // thread safe Zugriff durch synchroniserten Zugriff
     
       test()
       {
          o1.OnChange.connect( *this, &test::on_change );
          o2.OnChange.connect( *this, &test::on_change ); // << Problem, Signatur passt nicht
       }
    
       void on_change( observable<int> const& var )
       {
          ...
       }
    };
    

    Das Problem tritt in Zeile 9 auf, weil observable<int> und observable<int, synchronized_access<int>> unterschiedliche Typen sind. Gibt's da irgendeine schlaue Idee, wie ich den zweiten Template-Parameter verstecken kann? Ich könnte natürlich von einer Basisklasse ableiten, aber da hätte ich dann das Problem, dass der Rückgabewert der value() Funktion von der policy-Klasse abhängt und es schwierig macht, durch die Basisklasse an den Wert zu kommen.
    Oder bin ich hier generell schwer auf dem Holzweg und lauf in eine Sackgasse?



  • @DocShoe Ich würde es hier vielleicht mit einem Concept versuchen, das einen Typ-Parameter hat, mit dem geprüft wird, ob der Typ ein observable<T> mit einem beliebigen zweiten Template-Parameter ist.

    Das Concept könnte das entweder dadurch prüfen, dass die Klasseninstanzen entweder durch eine gemeinsame Basisklasse "markiert" sind, oder ob sie ein bestimmtes Interface implementieren (z.B. hat value und set_value, Member-Funktionen, value gibt ein T::accessor_return_type zurück, etc.).

    Dann könnte on_change so aussehen:

    void on_change( observable_concept<int> const& auto var )
    

    Letztendlich ist das nur die "coole" Variante von:

    template <typename T>
    void on_change( T const& var )
    

    Holzweg oder Sackgasse wird sich vermutlich erst im weiteren Code und in der Anwendung zeigen. Man wird den vollen Typen immer auf die eine oder andere Art mitführen und durchreichen müssen, wenn man da nicht irgendwo eine Type Erasure z.B. mit polymorphen Klassen zwischenschiebt.

    std::vector<int, custom_allocator<int>> wäre da ein Beispiel aus der Standardbibliothek, bei dem ich vermute, dass du eventuell irgendwann auf ähnlich gelagerte Probleme treffen könntest. Mit so einem Design kann man schon eine Menge machen, aber es hat auch seine Gründe, weshalb es Ansätze wie std::pmr::polymorphic_allocator gibt.



  • Finnegan hat es schon beschrieben. Es bliebe noch anzumerken falls es beim zweiten Parametern bleibt, sollte dieser zu einem Template Template Parameter geändert werden.

    Dann braucht man nicht observable<int, synchronized_access<int>>schreiben, sondern kann das auf observable<int, synchronized_access> kürzen.

    Dazu braucht man dann

    template<typename T, template<typename = T> typename accessor = immediate_access>
    class observable : public lockable
    …
    

    als neue Template Definition.



  • Danke für eure Antworten
    @Finnegan Ich bin auf C++17 beschränkt und kann keine Concepts benutzen

    @john-0
    Danke für die Info, die Umstellung auf einen template template Paramter habe ich schon, aber die Benutzung von Tals Default Template Parameter kannte ich noch nicht.

    Edit:
    Wenn's halt nicht anders geht, dann ist das halt so. Der Benutzer muss ja schließlich auch den Objekttypen vollständig qualifizieren, dann muss er das in seinen Callback halt auch tun. Vermutlich ist das auch nur eine hypothetische Frage, wäre halt schön gewesen, wenn man für alle oberservable Typen einen einzigen Callback registrieren könnte.



  • @DocShoe sagte in Signatur Klassen template:

    @john-0
    Danke für die Info, die Umstellung auf einen template template Paramter habe ich schon, aber die Benutzung von Tals Default Template Parameter kannte ich noch nicht.

    Man muss leider relativ lange suchen, bis man die korrekte Formulierung findet. Auch in dem an für sich sehr guten Buch von Josuttis und Vandevoorde C++ Templates steht das nicht drin. 😞


Anmelden zum Antworten