... wie man eine Vorlagenklasse mit unterschiedlichen Inhalten füllt, abhängig von den Werten der Vorlagenparameter?
Es war einmal, seit einiger Zeit wurde die D-Sprache unter Berücksichtigung der in C ++ gesammelten Erfahrungen als "das richtige C ++" bezeichnet. Im Laufe der Zeit ist D nicht weniger komplex und ausdrucksstärker geworden als C ++. Und bereits begann C ++, D auszuspionieren. Zum Beispiel erschien es in C ++ 17, if constexpr
meiner Meinung nach eine direkte Entlehnung von D ist, dessen Prototyp D-shny static if war .
Leider hat if constexpr
in C ++ nicht die gleiche Leistung wie static if
in D. Es gibt Gründe dafür , aber es gibt immer noch Fälle, in denen Sie nur bedauern können, dass if constexpr
in C ++ es Ihnen nicht erlaubt, den Inhalt von C + zu steuern + Klasse. Ich möchte über einen dieser Fälle sprechen.
Wir werden darüber sprechen, wie eine Vorlagenklasse erstellt wird, deren Inhalt (d. H. Die Zusammensetzung der Methoden und die Logik einiger Methoden) sich abhängig davon ändert, welche Parameter an diese Vorlagenklasse übergeben wurden. Ein Beispiel stammt aus dem wirklichen Leben, aus der Erfahrung mit der Entwicklung einer neuen Version von SObjectizer .
Die zu lösende Aufgabe
Es ist erforderlich, eine clevere Version des "intelligenten Zeigers" zum Speichern von Nachrichtenobjekten zu erstellen. Damit Sie etwas schreiben können wie:
message_holder_t<my_message> msg{ new my_message{...} }; send(target, msg); send(another_target, msg);
Der Trick dieser message_holder_t
Klasse besteht darin, dass drei wichtige Faktoren zu berücksichtigen sind.
Von welcher Art von Nachricht wurde geerbt?
Die Nachrichtentypen, die message_holder_t
parametrisieren, sind in zwei Gruppen unterteilt. Die erste Gruppe sind Nachrichten, die vom speziellen Basistyp message_t
erben. Zum Beispiel:
struct so5_message final : public so_5::message_t { int a_; std::string b_; std::chrono::milliseconds c_; so5_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} };
In diesem Fall sollte message_holder_t in sich nur einen Zeiger auf ein Objekt dieses Typs enthalten. Der gleiche Zeiger sollte in Getter-Methoden zurückgegeben werden. Das heißt, für den Erben von message_t
sollte es so etwas wie message_t
:
template<typename M> class message_holder_t { intrusive_ptr_t<M> m_msg; public: ... const M * get() const noexcept { return m_msg.get(); } };
Die zweite Gruppe sind Nachrichten beliebiger Benutzertypen, die nicht von message_t
geerbt message_t
. Zum Beispiel:
struct user_message final { int a_; std::string b_; std::chrono::milliseconds c_; user_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} };
Instanzen dieser Typen in SObjectizer werden nicht von sich aus gesendet, sondern sind in einem speziellen Wrapper eingeschlossen, user_type_message_t<M>
, der bereits von message_t
geerbt wurde. Daher muss message_holder_t
für solche Typen einen Zeiger auf user_type_message_t<M>
enthalten, und Getter-Methoden müssen einen Zeiger auf M zurückgeben:
template<typename M> class message_holder_t { intrusive_ptr_t<user_type_message_t<M>> m_msg; public: ... const M * get() const noexcept { return std::addressof(m_msg->m_payload); } };
Immunität oder Veränderlichkeit von Nachrichten
Der zweite Faktor ist die Aufteilung von Nachrichten in unveränderliche und veränderbare. Wenn die Nachricht unveränderlich ist (und standardmäßig unveränderlich ist), müssen Getter-Methoden einen konstanten Zeiger auf die Nachricht zurückgeben. Und wenn veränderlich, müssen Getter einen nicht konstanten Zeiger zurückgeben. Das heißt, sollte so etwas sein wie:
message_holder_t<so5_message> msg1{...};
shared_ptr vs unique_ptr
Der dritte Faktor ist die Logik des Verhaltens von message_holder_t
als intelligenter Zeiger. Sobald es sich wie std::shared_ptr
verhalten sollte, d.h. Sie können mehrere Nachrichteninhaber haben, die auf dieselbe Nachrichteninstanz verweisen. Und sobald es sich wie std::unique_ptr
verhalten sollte, d.h. Nur eine message_holder-Instanz kann auf eine Nachrichteninstanz verweisen.
Standardmäßig sollte das Verhalten von message_holder_t
von der Veränderbarkeit / Unveränderlichkeit der Nachricht abhängen. Das heißt, Bei unveränderlichen Nachrichten sollte sich message_holder_t
wie std::shared_ptr
verhalten, und bei veränderlichen std::unique_ptr
wie std::unique_ptr
:
message_holder_t<so5_message> msg1{...}; message_holder_t<so5_message> msg2 = msg;
Das Leben ist jedoch eine komplizierte Sache, daher müssen Sie auch in der Lage sein, das Verhalten von message_holder_t
manuell message_holder_t
. Damit können Sie message_holder für eine unveränderliche Nachricht erstellen, die sich wie unique_ptr verhält. Und damit Sie message_holder für eine veränderbare Nachricht erstellen können, die sich wie shared_ptr verhält:
using unique_so5_message = so_5::message_holder_t< so5_message, so_5::message_ownership_t::unique>; unique_so5_message msg1{...}; unique_so5_message msg2 = msg1;
Wenn message_holder_t
wie shared_ptr funktioniert, sollte es message_holder_t
die üblichen Konstruktoren und Zuweisungsoperatoren enthalten: sowohl Kopieren als auch Verschieben. Außerdem muss es eine konstante Methode make_reference
, die eine Kopie des in message_holder_t
gespeicherten Zeigers message_holder_t
.
Wenn message_holder_t
wie unique_ptr funktioniert, sollten der Konstruktor und der message_holder_t
dafür message_holder_t
werden. Und die Methode make_reference
sollte den Zeiger vom Objekt message_holder_t
: Nach dem Aufruf von make_reference
ursprüngliche message_holder_t
leer bleiben.
Sie müssen also eine Vorlagenklasse erstellen:
template< typename M, message_ownership_t Ownership = message_ownership_t::autodetected> class message_holder_t {...};
welche:
- innen sollte
intrusive_ptr_t<M>
oder intrusive_ptr<user_type_message_t<M>>
gespeichert werden, je nachdem, ob M von message_t
geerbt message_t
; - Getter-Methoden müssen je nach Veränderbarkeit / Unveränderlichkeit der Nachricht entweder
const M*
oder M*
. - Es sollte entweder einen vollständigen Satz von Konstruktoren und Kopier- / Verschiebungsoperatoren oder nur einen Konstruktor- und Verschiebungsoperator geben.
- Die Methode
make_reference()
sollte entweder eine Kopie des gespeicherten intrusive_ptr zurückgeben oder den Wert von intrusive_ptr message_holder_t
und den ursprünglichen message_holder_t
leer lassen. Im ersten Fall muss make_reference()
konstant sein, im zweiten Fall - nicht konstant.
Die letzten beiden Elemente aus der Liste werden durch den Eigentümerparameter bestimmt (sowie durch die Veränderbarkeit der Nachricht, wenn autodetected
für die Eigentümerschaft verwendet wird).
Wie es entschieden wurde
In diesem Abschnitt werden alle Komponenten betrachtet, aus denen die endgültige Lösung besteht. Nun, die resultierende Lösung selbst. Die Codefragmente, die von allen störenden Details befreit sind, werden angezeigt. Wenn sich jemand für den echten Code interessiert, können Sie ihn hier sehen .
Haftungsausschluss
Die unten gezeigte Lösung gibt nicht vor, schön, ideal oder ein Vorbild zu sein. Es wurde in kurzer Zeit unter Termindruck gefunden, implementiert, getestet und dokumentiert. Vielleicht, wenn es mehr Zeit gab und mehr nach einer Lösung suchte jung Sinnvoll und kenntnisreich in modernen C ++ - Entwicklern, würde es sich als kompakter, einfacher und verständlicher herausstellen. Aber wie sich herausstellte, passierte es im Allgemeinen ... "Erschieß den Pianisten nicht".
Abfolge von Schritten und vorgefertigte Vorlagenmagie
Wir brauchen also eine Klasse mit mehreren Methoden. Der Inhalt dieser Kits muss von irgendwoher kommen. Woher?
In D könnten wir static if
und abhängig von verschiedenen Bedingungen verschiedene Teile der Klasse definieren. In einigen Ruby-Versionen können wir mithilfe der include-Methode Methoden in unsere Klasse mischen . Wir befinden uns jedoch in C ++, wo unsere Möglichkeiten bisher sehr begrenzt sind: Wir können entweder eine Methode / ein Attribut direkt in der Klasse definieren oder die Methode / das Attribut von einer Basisklasse erben.
Wir können abhängig von einer bestimmten Bedingung keine unterschiedlichen Methoden / Attribute innerhalb der Klasse definieren, weil C ++ if constexpr
kein D static if
. Folglich bleibt nur die Vererbung übrig.
Upd. Wie in den Kommentaren vorgeschlagen, sollte ich hier genauer sprechen. Da C ++ über SFINAE verfügt, können wir die Sichtbarkeit einzelner Methoden in der Klasse über SFINAE aktivieren / deaktivieren (d. H. Einen Effekt erzielen, der dem static if
ähnelt). Dieser Ansatz weist meiner Meinung nach zwei schwerwiegende Mängel auf. Erstens, wenn solche Methoden nicht 1-2-3, sondern 4-5 oder mehr sind, ist es mühsam, jede mit SFINAE zu formatieren, was die Lesbarkeit des Codes beeinträchtigt. Zweitens hilft uns SFINAE nicht beim Hinzufügen / Entfernen von Klassenattributen (Feldern).
In C ++ können wir mehrere Basisklassen definieren, von denen wir dann message_holder_t
erben. Die Auswahl der einen oder anderen Basisklasse erfolgt bereits in Abhängigkeit von den Werten der Vorlagenparameter unter Verwendung von std :: conditional .
Der Trick ist jedoch, dass wir nicht nur eine Reihe von Basisklassen benötigen, sondern eine kleine Vererbungskette. Zu Beginn wird es eine Klasse geben, die die allgemeine Funktionalität bestimmt, die in jedem Fall erforderlich ist. Als nächstes folgen die Basisklassen, die die Logik des Verhaltens des "intelligenten Zeigers" bestimmen. Und dann wird es eine Klasse geben, die die notwendigen Getter bestimmt. In dieser Reihenfolge betrachten wir die implementierten Klassen.
Unsere Aufgabe wird durch die Tatsache vereinfacht, dass SObjectizer bereits über eine vorgefertigte Vorlagenmagie verfügt , die bestimmt, ob eine Nachricht von message_t geerbt wird , sowie über Mittel zum Überprüfen der Nachrichtenveränderlichkeit . Daher werden wir bei der Implementierung einfach diese vorgefertigte Magie verwenden und nicht auf die Details ihrer Arbeit eingehen.
Gemeinsame Zeigerspeicherbasis
Beginnen wir mit einem gemeinsamen Basistyp, der den entsprechenden intrusive_ptr speichert und außerdem einen allgemeinen Satz von Methoden bereitstellt, die für jede der Implementierungen von message_holder_t
erforderlich sind:
template< typename Payload, typename Envelope > class basic_message_holder_impl_t { protected : intrusive_ptr_t< Envelope > m_msg; public : using payload_type = Payload; using envelope_type = Envelope; basic_message_holder_impl_t() noexcept = default; basic_message_holder_impl_t( intrusive_ptr_t< Envelope > msg ) noexcept : m_msg{ std::move(msg) } {} void reset() noexcept { m_msg.reset(); } [[nodiscard]] bool empty() const noexcept { return static_cast<bool>( m_msg ); } [[nodiscard]] operator bool() const noexcept { return !this->empty(); } [[nodiscard]] bool operator!() const noexcept { return this->empty(); } };
Diese Vorlagenklasse hat zwei Parameter. Die erste, Payload, legt den Typ fest, den Getter-Methoden verwenden sollen. Während der zweite, Envelope, den Typ für intrusive_ptr festlegt. Wenn der Nachrichtentyp von message_t
geerbt message_t
beide Parameter denselben Wert. Wenn die Nachricht jedoch nicht von message_t
geerbt message_t
, wird der Nachrichtentyp als Payload und user_type_message_t<Payload>
als Envelope verwendet.
Ich denke, dass der Inhalt dieser Klasse im Grunde keine Fragen aufwirft. Zwei Dinge sollten jedoch getrennt beachtet werden.
Erstens ist der Zeiger selbst, d.h. Das Attribut m_msg wird im geschützten Abschnitt definiert, damit die Klassenvererben Zugriff darauf haben.
Zweitens generiert der Compiler für diese Klasse selbst alle erforderlichen Konstruktoren und Kopier- / Verschiebungsoperatoren. Und auf der Ebene dieser Klasse verbieten wir noch nichts.
Separate Basen für das Verhalten von shared_ptr und unique_ptr
Wir haben also eine Klasse, die einen Zeiger auf eine Nachricht speichert. Jetzt können wir seine Erben definieren, die sich entweder als shared_ptr oder als unique_ptr verhalten.
Beginnen wir mit dem Fall des Verhaltens von shared_ptr, weil Hier ist der kleinste Code:
template< typename Payload, typename Envelope > class shared_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() const noexcept { return this->m_msg; } };
Nichts Kompliziertes: Erben Sie von basic_message_holder_impl_t
, erben Sie alle Konstruktoren und definieren Sie eine einfache, zerstörungsfreie Implementierung von make_reference()
.
Für den Fall von unique_ptr-Verhalten ist der Code größer, obwohl nichts Kompliziertes darin ist:
template< typename Payload, typename Envelope > class unique_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; unique_message_holder_impl_t( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t( unique_message_holder_impl_t && ) = default; unique_message_holder_impl_t & operator=( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t & operator=( unique_message_holder_impl_t && ) = default; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() noexcept { return { std::move(this->m_msg) }; } };
Wieder erben wir von basic_message_holder_impl_t
und erben die Konstruktoren, die wir benötigen (dies ist der Standardkonstruktor und der initialisierende Konstruktor). Gleichzeitig definieren wir die Konstruktoren und Kopier- / Verschiebungsoperatoren gemäß der Logik unique_ptr: Wir verbieten das Kopieren, wir implementieren die Verschiebung.
Wir haben hier auch eine destruktive make_reference()
-Methode.
Das ist eigentlich alles. Es bleibt nur die Wahl zwischen diesen beiden Basisklassen zu realisieren ...
Wählen Sie zwischen shared_ptr und unique_ptr
Um zwischen dem Verhalten von shared_ptr und unique_ptr zu wählen, benötigen Sie die folgende Metafunktion (Metafunktion, da sie mit Typen in der Kompilierungszeit "funktioniert"):
template< typename Msg, message_ownership_t Ownership > struct impl_selector { static_assert( !is_signal<Msg>::value, "Signals can't be used with message_holder" ); using P = typename message_payload_type< Msg >::payload_type; using E = typename message_payload_type< Msg >::envelope_type; using type = std::conditional_t< message_ownership_t::autodetected == Ownership, std::conditional_t< message_mutability_t::immutable_message == message_mutability_traits<Msg>::mutability, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> >, std::conditional_t< message_ownership_t::shared == Ownership, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> > >; };
Diese Metafunktion akzeptiert beide Parameter aus der Parameterliste message_holder_t
und gibt als Ergebnis ( message_holder_t
die Definition eines verschachtelten type
) den Typ zurück, von dem er geerbt werden soll. Das heißt, entweder shared_message_holder_impl_t
oder unique_message_holder_impl_t
.
In der Definition von impl_selector
Sie Spuren der oben erwähnten Magie, auf die wir nicht message_payload_type<Msg>::payload_type
: message_payload_type<Msg>::payload_type
, message_payload_type<Msg>::envelope_type
und message_mutability_traits<Msg>::mutability
.
Und um die impl_selector
einfacher nutzen zu können, definieren wir einen kürzeren Namen dafür:
template< typename Msg, message_ownership_t Ownership > using impl_selector_t = typename impl_selector<Msg, Ownership>::type;
Basis für Getter
Wir haben also bereits die Möglichkeit, eine Basis auszuwählen, die einen Zeiger enthält und das Verhalten eines "intelligenten Zeigers" definiert. Jetzt müssen wir diese Basis mit Getter-Methoden versehen. Warum brauchen wir eine einfache Klasse:
template< typename Base, typename Return_Type > class msg_accessors_t : public Base { public : using Base::Base; [[nodiscard]] Return_Type * get() const noexcept { return get_ptr( this->m_msg ); } [[nodiscard]] Return_Type & operator * () const noexcept { return *get(); } [[nodiscard]] Return_Type * operator->() const noexcept { return get(); } };
Dies ist eine Vorlagenklasse, die von zwei Parametern abhängt, deren Bedeutung jedoch völlig unterschiedlich ist. Der Basisparameter ist das Ergebnis der oben gezeigten impl_selector
impl_selector. Das heißt, Als Basisparameter wird die Basisklasse festgelegt, von der geerbt werden soll.
Es ist wichtig zu beachten, dass der Compiler den Konstruktor und den msg_accessors_t
für msg_accessors_t
nicht generieren kann, wenn die Vererbung von unique_message_holder_impl_t
stammt, für die der Konstruktor und der unique_message_holder_impl_t
verboten sind. Welches ist was wir brauchen.
Der Typ der Nachricht, dessen Zeiger / Link von Gettern zurückgegeben wird, fungiert als Return_Type-Parameter. Der Trick besteht darin, dass für eine unveränderliche Nachricht vom Typ Msg
der Parameter Return_Type auf const Msg
. Während für eine veränderbare Nachricht vom Typ Msg
Parameter Return_Type den Wert Msg
. Daher gibt die Methode get()
const Msg*
für unveränderliche Nachrichten und nur Msg*
für veränderbare Nachrichten zurück.
Mit der kostenlosen Funktion get_ptr()
lösen get_ptr()
das Problem der Arbeit mit Nachrichten, die nicht von message_t
geerbt message_t
:
template< typename M > M * get_ptr( const intrusive_ptr_t<M> & msg ) noexcept { return msg.get(); } template< typename M > M * get_ptr( const intrusive_ptr_t< user_type_message_t<M> > & msg ) noexcept { return std::addressof(msg->m_payload); }
Das heißt, Wenn die Nachricht nicht von message_t
geerbt und als user_type_message_t<Msg>
gespeichert wird, wird die zweite Überladung aufgerufen. Und wenn es vererbt wird, dann die erste Überlastung.
Auswahl einer bestimmten Basis für Getter
msg_accessors_t
Vorlage msg_accessors_t
erfordert also zwei Parameter. Die erste wird durch die impl_selector
impl_selector berechnet. Um jedoch aus msg_accessors_t
einen bestimmten Basistyp zu bilden, müssen wir den Wert des zweiten Parameters bestimmen. Eine weitere Metafunktion ist dafür vorgesehen:
template< message_mutability_t Mutability, typename Base > struct accessor_selector { using type = std::conditional_t< message_mutability_t::immutable_message == Mutability, msg_accessors_t<Base, typename Base::payload_type const>, msg_accessors_t<Base, typename Base::payload_type> >; };
Sie können nur auf die Berechnung des Return_Type-Parameters achten. Einer dieser wenigen Fälle, in denen East Const nützlich ist;)
Um die Lesbarkeit des folgenden Codes zu verbessern, sollten Sie eine kompaktere Option für die Arbeit damit verwenden:
template< message_mutability_t Mutability, typename Base > using accessor_selector_t = typename accessor_selector<Mutability, Base>::type;
Letzter Nachfolger message_holder_t
Jetzt können Sie sich ansehen, was message_holder_t
, für dessen Implementierung alle diese Basisklassen und Metafunktionen erforderlich waren (ein Teil der Methoden zum Erstellen einer Instanz der in message_holder gespeicherten Nachricht wird aus der Implementierung entfernt):
template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t : public details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> > { using base_type = details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> >; public : using payload_type = typename base_type::payload_type; using envelope_type = typename base_type::envelope_type; using base_type::base_type; friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.message_reference(), b.message_reference() ); } };
Tatsächlich war alles, was wir oben analysiert haben, erforderlich, um diesen „Aufruf“ von zwei Metafunktionen aufzuzeichnen:
details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> >
Weil Dies ist nicht die erste Option, aber das Ergebnis der Vereinfachung und Reduzierung des Codes kann ich sagen, dass kompakte Formen von Metafunktionen die Menge des Codes stark reduzieren und seine Verständlichkeit erhöhen (wenn es allgemein angebracht ist, hier über Verständlichkeit zu sprechen).
Und was würde passieren, wenn ...
Aber wenn in C ++ if constexpr
so mächtig war wie static if
in D, dann könnten Sie etwas schreiben wie:
Hypothetische Version mit fortgeschrittener wenn constexpr template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t { static constexpr const message_mutability_t Mutability = details::message_mutability_traits<Msg>::mutability; static constexpr const message_ownership_t Actual_Ownership = (message_ownership_t::unique == Ownership || (message_mutability_t::mutable_msg == Mutability && message_ownership_t::autodetected == Ownership)) ? message_ownership_t::unique : message_ownership_t::shared; public : using payload_type = typename message_payload_type< Msg >::payload_type; using envelope_type = typename message_payload_type< Msg >::envelope_type; private : using getter_return_type = std::conditional_t< message_mutability_t::immutable_msg == Mutability, payload_type const, payload_type >; public : message_holder_t() noexcept = default; message_holder_t( intrusive_ptr_t< envelope_type > mf ) noexcept : m_msg{ std::move(mf) } {} if constexpr(message_ownership_t::unique == Actual_Ownership ) { message_holder_t( const message_holder_t & ) = delete; message_holder_t( message_holder_t && ) noexcept = default; message_holder_t & operator=( const message_holder_t & ) = delete; message_holder_t & operator=( message_holder_t && ) noexcept = default; } friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.m_msg, b.m_msg ); } [[nodiscard]] getter_return_type * get() const noexcept { return get_const_ptr( m_msg ); } [[nodiscard]] getter_return_type & operator * () const noexcept { return *get(); } [[nodiscard]] getter_return_type * operator->() const noexcept { return get(); } if constexpr(message_ownership_t::shared == Actual_Ownership) { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() const noexcept { return m_msg; } } else { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() noexcept { return { std::move(m_msg) }; } } private : intrusive_ptr_t< envelope_type > m_msg; };
, . C++ :(
( C++ "" ).
, , ++. , , , . , message_holder_t
. , , if constexpr
.
Fazit
, C++. , . , , .
, .
, , ++ , . , . , , . , . C++98/03 , C++11 .