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>
undobservable<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 dervalue()
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
undset_value
, Member-Funktionen,value
gibt einT::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 wiestd::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 aufobservable<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 vonT
als 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 vonT
als 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.