Member von Strukturen per SFINAE löschen
-
@DocShoe sagte in Member von Strukturen per SFINAE löschen:
@Finnegan
Sobald ich von irgendwas erbe spendiere ich der Basisklasse nen virtuellen Destruktor.ContextData
ist in diesem Fall ja auch nur eine Zwischenklasse, von derData
erbt, in meinem Spielprojekt istData
schon polymorph.
Nen richtigen Anwendungsfall für das Problem habe ich eigentlich nicht, habe nur etwas rumgespielt. Ziel war es, einen Container für iwas zu haben, wo man dem iwas optional irgendwelche Kontextinformationen zuordnen kann, die mit dem iwas eigentlich nichts zu tun haben. Und wie man den Container dann ohne Kontextinformationen umsetzt, davoid
als Kontextdatentyp besonders ist.Da in C++ Vererbung auch ohne Polymorphie möglich ist, und eine virtuelle Methode einen minimalen potentiellen Overhead hat (VTable-Lookup), ist meine Philosophie eher die Klasse nur dann polymorph zu machen, wenn ich auch wirklich dynamische Bindung haben will (also sowas wie
base->derived_member_function()
) oder ich ein Objekt einer abgeleiteten Klasse über die Basisklasse löschen will (Base* b = new Derived; delete b;
).Und auch wenn
Data
polymorph ist, dann muss das nicht zwangsläufig auch jede Klasse sein, von derData
erbt. Man kann die Polymorphie durchaus auch erst tiefer in der Klassenhiercharchie einführen.Es ist aber denke ich auch kein allzu grosses Problem, die Basisklasse hier polymorph zu machen. Der Compiler wird das wahrscheinlich ohnehin de-virtualisieren, womit dann auch der kleine Overhead verschwände. Ich versuche aber immer, selbst vernachlässigbare Overheads zu vermeiden, wenn sie nicht unbedingt notwendig sind. Irgendwann summieren die sich vielleicht auf und man hat ein Programm, das "irgendwie langsam" ist, aber keinen wirklichen "Hot Path" an dem man das festmachen und den man optimieren könnte.
Das erachte ich auch nicht als Mikro-Optimierung, für mich ist das nur eines der Standard-Pattern, mit denen ich Code formuliere. Sowas wie z.B. einmal eine Referenz auf ein Map-Element zu holen (
auto& element = map[key];
) und das dann zu nutzen, anstatt in Folge mehrfachmap[key]
zu verwenden
-
Da muss ich leider zugeben, dass mir an manchen Stellen das Wissen einfach fehlt. Das sind so Dinge, die man irgendwann mal behandelt hat und die Details dazu wieder vergessen hat. Sobald man von irgendwas erbt macht man mit einem virtuellen Destruktor erst mal nichts falsch. Wenn's dann ohne vtable geht isses Bonus mit Fleißkärtchen. Wir schreiben keine Software wie google oder facebook, wo man jeden CPU Zyklus einsparen will, wobei ich generell aber schon drauf achte, dass ich nicht absichtlich langsamen Code schreibe.
-
@DocShoe sagte in Member von Strukturen per SFINAE löschen:
Sobald man von irgendwas erbt macht man mit einem virtuellen Destruktor erst mal nichts falsch.
Das stimmt.
Wenn's dann ohne vtable geht isses Bonus mit Fleißkärtchen. Wir schreiben keine Software wie google oder facebook, wo man jeden CPU Zyklus einsparen will, wobei ich generell aber schon drauf achte, dass ich nicht absichtlich langsamen Code schreibe.
Und es sollte auch nur ein Denkanstoß sein. Ich hoffe es wurde deutlich, dass die Auswirkungen meistens (wenn überhaupt) nur minimal sind.
-
Hm, ich bin da gerade ein bisschen zwiegespalten. Der Vorteil, einer Basisklasse immer einen virtuellen Destruktor zu verpassen ist, wenn doch mal jemand (anderes vielleicht) auf die Idee kommt, die Klasse als polymorphe Basisklasse zu verwenden, muss der nicht daran denken, dass es da ein Problem geben könnte. Man hat also eine potentielle zukünftige Fehlerquelle weniger.
Auf der anderen Seite verhindet ein Destruktor die Compiler generierten Move Assignement und Move Ctor und man handelt sich den VTable Overhead ein.
-
@Finnegan sagte in Member von Strukturen per SFINAE löschen:
Da in C++ Vererbung auch ohne Polymorphie möglich ist, und eine virtuelle Methode einen minimalen potentiellen Overhead hat (VTable-Lookup), ist meine Philosophie eher die Klasse nur dann polymorph zu machen, wenn ich auch wirklich dynamische Bindung haben will (also sowas wie
base->derived_member_function()
) oder ich ein Objekt einer abgeleiteten Klasse über die Basisklasse löschen will (Base* b = new Derived; delete b;
).Das birgt aber enorme Gefahren, dass das jemand missbraucht bzw. es gar nicht merkt, dass er es missbraucht, und damit nicht zu Recht kommt. C++ ist auch so schon kompliziert genug, so dass man hier doch eher zur Komposition ggf. durch Templates zurückgreifen sollte. Dann ist klarer was man eigentlich will.
Und auch wenn
Data
polymorph ist, dann muss das nicht zwangsläufig auch jede Klasse sein, von derData
erbt. Man kann die Polymorphie durchaus auch erst tiefer in der Klassenhiercharchie einführen.Da öffnest Du aber wirklich Tür und Tor dafür, dass jemand das nicht korrekt nutzt.
Es ist aber denke ich auch kein allzu grosses Problem, die Basisklasse hier polymorph zu machen. Der Compiler wird das wahrscheinlich ohnehin de-virtualisieren, womit dann auch der kleine Overhead verschwände. Ich versuche aber immer, selbst vernachlässigbare Overheads zu vermeiden, wenn sie nicht unbedingt notwendig sind.
Man sollte lieber keine Mikrooptimierungen nutzen, wenn dadurch das Design klarer wird.
-
Um mal auf das eigentliche Problem zurückzukommen:
Ich persönlich nutze meist eher einen leicht anderen Ansatz. Dieses Konstrukt, mit einer bestimmten Basisklasse empfinde ich als unpraktisch und kommuniziert mir persönlich meist das Falsche. Entwickler, die eher aus anderen Sprachen wie Java kommen, schauen sich meiner Erfahrung nach die Klasse an und verwerten alles, was sie meinen zu verstehen. Leider ist eine simple Vererbung einfach genug zu verstehen, sodass dann die Konzepte aus anderen Sprachen wiederverwertet werden, was dann letztendlich zu der immer wieder kommenden Diskussion bzgl. virtual DTor usw. führt.template <typename DataPolicy> struct BasicData : public DataPolicy { unsigned int Id{}; template <typename... Args> BasicData(int id, Args&&... args) : DataPolicy{std::forward<Args>(args)...} { } };
Statt mich auf eine explizite Basisklasse (mit festen Eigenschaften) festzulegen, nutze ich hier eine template Klasse. Das verschiebt zum einen die wichtigen Details in die Policy und zum einen kann diese einfach (Situationsabhängig) ausgetauscht werden.
Eine passende Policy sähe dann z.B. folgendermaßen aus:template <typename T> struct ContextData { public: const T& Context() const noexcept { return m_Context; } protected: ~ContextData() = default; ContextData(T ctx) : m_Context{std::move(ctx)} { } ContextData(const ContextData&) = default; ContextData& operator =(const ContextData&) = default; ContextData(ContextData&&) = default; ContextData& operator =(ContextData&&) = default; private: T m_Context{}; }; template <> struct ContextData<void> { protected: ~ContextData() = default; ContextData() = default; ContextData(const ContextData&) = default; ContextData& operator =(const ContextData&) = default; ContextData(ContextData&&) = default; ContextData& operator =(ContextData&&) = default; };
Man kann sich hier vieles zu Nutze machen; daher sieht das eigentlich ziemlich ähnlich zu deiner Lösung aus. Der wesentliche Punkt ist aber, dass man von außen eine andere Basisklasse injecten kann und damit als Nutzer die Eigenschaften von
BasicData
steuert, ohne selbst explizit vonData
erben zu müssen. Wie man sieht, habe ich hier auch noch die Eigenschaften eingefügt, dass einContextData
nur über die erbende Klasse zerstört werden kann (protected DTor). Das lässt sich beliebig granular steuern, erhöht natürlich etwas die Komplexität der Lösung. Aber wenn man einen Member nur unter speziellen Umständen enablen möchte, dann ist man imo in der Komplexität schon eh ein wenig weiter vorgedrungen; dann sollte man es auch richtig machen und dazu gehören dann auch die Gedanken bzgl. Copy, Move, DTor dazu (wurde ja bereits oben besprochen).Möchte man nun echte Polymorphie enablen, dann reicht es eine
PolymorphicContextData
Policy zu basteln und diese entsprechend zu verwenden. Damit macht man nichts kaputt, dass bereits den die vorhandeneBasicData<ContextData>
Kombo benutzt, kann sich aber meist dennoch das vorhandene (templatisierte) Ökosystem zunutze machen.Das Ganze lässt sich im Endeffekt auch für beliebig viele
DataPolicies
erweitern (Stichwortvariadic templates
), erhöht dann aber auch wieder etwas die Komplexität, da möglicher Weise auch Sonderfälle betrachtet werden müssen.Nicht falsch verstehen, dieser Ansatz ist nicht komplizierter um des kompliziert seins Willen, sondern bietet mir persönlich meist eine beruhigende Flexibilität, sodass ich für Änderungen gewappnet bin. Der zweite Vorteil ist für mich, dass es exakt so viel Unverständnis erzeugt, sodass ihn Kollegen nicht missbrauchen, sondern als Implementierungsdetail akzeptieren.
Hier mal ein kleines Beispiel: https://godbolt.org/
-
@john-0 sagte in Member von Strukturen per SFINAE löschen:
Da öffnest Du aber wirklich Tür und Tor dafür, dass jemand das nicht korrekt nutzt.
Wenn das eine öffentliche Klasse ist, die für den Anwender frei zugänglich ist, dann ja.
ContextData<B, T>
sieht mir aber eher nach einer internen Klasse aus, die nicht Teil des Public Interface ist und die man wahrscheinlich auch in einem anonymen Namespace unterbringen würde.Ich würde auf Anhieb nicht vermuten, dass im Code außerhalb von
Data
-Memberfunktionen irgendwo mit einemContextData<B, T>
hantiert wird oder dass ein Anwender überhaupt weiß, dass eine solche Klasse existiert. In dem Fall hielte sich das Missbrauchspotential doch arg in Grenzen.Oder anders: Sollte man wirklich jedes simple POD-
struct
(als dass ichContextData<B, T>
hier erachte) mit einem virtuellen Destruktor versehen, nur weil damit mal jemand komische Sachen machen könnte? Konzepte wie "POD" und "Standard Layout" wären damit nämlich hinfällig.Auch ein Großteil der Standardbibliothek-Klassen ist nicht polymorph, obwohl damit wohl jeder Programmierer in Berührung kommt und Blödsinn damit machen könnte. Dennoch scheinen wir nur selten darüber zu diskutieren, dass es gefährlich sein könnte, wenn jemand von
std::string
ableitet und dann versucht, ein Objekt dieses Typs über einenstd::basic_string*
zu löschen.Man sollte lieber keine Mikrooptimierungen nutzen, wenn dadurch das Design klarer wird.
Man kann sich sicher darüber streiten, ob das Design wirklich "klarer" wird, wenn eine Klasse, bei der Polymorphie nicht vorgesehen ist, einem durch den virtuellen Destruktor eben das genaue Gegenteil sagt.
Ich habe auch oben argumentiert, dass ich das eben nich* als eine Mikrooptimierung erachte, sondern eher als ein natürliches Code-Design für Klassen, bei denen eben nicht vorgesehen ist, dass man sie im Programm als "Handle" in die tiefere Objekthierarchie verwendet (sprich als Referenz- oder Pointer-Typ für davon abgeleitete dynamische Typen). Damit das nicht versehentlich passiert kann man gerne zusätzliche Maßnahmen treffen, wenn der klobige Name (
ContextData<B, T>
) nicht schon ausreichend sein sollte: Anonymer Namespace, Klasse als privater Member, auch Deklaration in Übersetzungseinheit, etc.
-
@DocShoe sagte in Member von Strukturen per SFINAE löschen:
Hat da jemand ne Idee?
Wie wäre es mit diesem Lösungsansatz?
#include <type_traits> #include <iostream> template<typename T> struct DataVoid { int Id = 0; }; template<typename T> struct DataT { int Id = 0; T t = 1; }; template<typename T, template<typename> typename T1 = DataVoid, template<typename> typename T2 = DataT> struct Data { using DT = std::conditional<std::is_same_v<T, void>, T1<T>, T2<T>>::type; DT data; }; int main() { Data<int> d1; // OK Data<void> d2; // std::cout << "d1: " << d1.data.Id << ", " << d1.data.t << "\n"; std::cout << "d2: " << d2.data.Id << "\n"; }
-
@john-0 sagte in Member von Strukturen per SFINAE löschen:
Wie wäre es mit diesem Lösungsansatz?
Das umgeht die offenbar unbeliebte Vererbung und verschwendet auch kein Byte, falls es keinen
Context
gibt, macht die Datenstruktur aber auch etwas klobig in der Anwendung (data.data.Id
). Ich persönlich wäre kein Fan davon, aber mit Gettern ließe sich das immerhin verstecken (data.getId()
).Man könnte das auch noch eine Ebene höher heben, indem man
DT
einfachData
nennt und außerhalb der Klasse deklariert. Dann läuft man aber Gefahr, Funktionen mehrfach implementieren zu müssen, wenn man weiterhin Vererbung vermeiden will.
-
So die nächste Variante ohne die Nachteile von vorher.
#include <type_traits> #include <iostream> template<class T> class Data; template<class T> requires std::is_same_v<T, void> class Data<T> { public: int Id = 0; }; template<class T> requires (! std::is_same_v<T, void>) class Data<T> { public: int Id = 0; T t = 1; }; int main() { Data<int> d1; // OK Data<void> d2; // OK std::cout << "d1: " << d1.Id << ", " << d1.t << "\n"; std::cout << "d2: " << d2.Id << "\n"; }
-
Vielen Dank für die verschiedenen Ansätze und Lösungen!
Ich habe aktuell kein konkretes Problem, für das ich das hier brauche, die Frage entstand aus Neugier. Leider habe ich im Moment auch wenig Zeit, als dass ich die verschiedenen Lösung ausprobieren könnte, ich werde aber drauf zurückkommen, wenn ich mich damit beschäftigt habe.
-
@john-0 sagte in Member von Strukturen per SFINAE löschen:
So die nächste Variante ohne die Nachteile von vorher.
#include <type_traits> #include <iostream> template<class T> class Data; template<class T> requires std::is_same_v<T, void> class Data<T> { public: int Id = 0; }; template<class T> requires (! std::is_same_v<T, void>) class Data<T> { public: int Id = 0; T t = 1; }; int main() { Data<int> d1; // OK Data<void> d2; // OK std::cout << "d1: " << d1.Id << ", " << d1.t << "\n"; std::cout << "d2: " << d2.Id << "\n"; }
Hat es denn einen besonderen Grund, warum du das mit einem komplizierten Constraint, anstatt einfach einer normalen specialization löst?
Das macht es doch nochmal um ein vielfaches cleaner.template<class T> class Data{ public: int Id = 0; T t = 1; }; template<> class Data<void> { public: int Id = 0; };
-
@DNKpp sagte in Member von Strukturen per SFINAE löschen:
Hat es denn einen besonderen Grund, warum du das mit einem komplizierten Constraint, anstatt einfach einer normalen specialization löst?
Es würde nach eine SFINAE Lösung gefragt, und bei SFINAE kann man eben nicht nur den Typen als Unterscheidung nutzen, wie bei der Spezialisierung. Nehmen wir mal an, dass wir eine andere Anforderung haben z.B. dass der Datentyp die Bedingungen eines Körpers erfüllt.
#include <type_traits> #include <iostream> #include <string> template <typename T> concept Addition = requires (T a, T b) {a + b;}; template <typename T> concept Substraction = requires (T a, T b) {a - b;}; template <typename T> concept Multiplication = requires (T a, T b) {a * b;}; template <typename T> concept Division = requires (T a, T b) {a / b;}; template <typename T> concept Field = Addition<T> && Substraction<T> && Multiplication<T> && Division<T> && std::regular<T>; template<class T> class Data; template<class T> requires (!Field<T>) class Data<T> { public: int Id = 0; }; template<class T> requires Field<T> class Data<T> { public: int Id = 0; T t = 1; }; int main() { Data<int> d1; // OK Data<std::string> d2; // OK std::cout << "d1: " << d1.Id << ", " << d1.t << "\n"; std::cout << "d2: " << d2.Id << "\n"; }
-
@john-0 sagte in Member von Strukturen per SFINAE löschen:
@DNKpp sagte in Member von Strukturen per SFINAE löschen:
Hat es denn einen besonderen Grund, warum du das mit einem komplizierten Constraint, anstatt einfach einer normalen specialization löst?
Es würde nach eine SFINAE Lösung gefragt, und bei SFINAE kann man eben nicht nur den Typen als Unterscheidung nutzen, wie bei der Spezialisierung. Nehmen wir mal an, dass wir eine andere Anforderung haben z.B. dass der > Datentyp die Bedingungen eines Körpers erfüllt.
Das mag wohl sein, trotzdem ist es doch recht umständlich ein Primary Template + zwei Spezialisierungen zu verwenden. Immerhin prüfst du auf "hat Eigenschaft" und "hat Eigenschaft nicht". Das mag für dieses binäre Verhalten noch klar gehen, skaliert aber echt schlecht.
template<class T> class Data { public: int Id = 0; }; template<Field T> class Data<T> { public: int Id = 0; T t = 1; };
-
@DNKpp sagte in Member von Strukturen per SFINAE löschen:
Das mag wohl sein, trotzdem ist es doch recht umständlich ein Primary Template + zwei Spezialisierungen zu verwenden.
Es ist der Weg, den sich die Macher von C++ ausgedacht haben. Es ist nicht so effizient wie
template<typename T> struct Data { int Id = 0; #ifdef T_IS_NOT_VOID T t = 1; #endif };
. Es geht auch nicht über constexpr if
#include <iostream> template <typename T> struct Data { int Id = 0; if constexpr (std::is_same_v<T, void>) { T t = 1; } }; int main() { Data<int> d1; Data<void> d2; std::cout << "d1: " << d1.Id << ", " << d1.t << "\n"; std::cout << "d2: " << d2.Id << "\n"; }
Man kann in vielen Fällen constexpr if verwenden, aber nicht in einem Fall in dem man einen Member definieren will.
-
Außerdem löschen eure Lösungen doch gar nicht den Member, sondern ihr kopiert einfach die Klasse und fügt in der "non-void"-Klasse den zusätzlichen Member hinzu.
Es geht ja darum bei einer beliebigen Klasse (mit beliebig vielen Membern und Memberfunktionen) einen (oder mehrere) Member zu "löschen" (d.h. keinen Speicherplatz dafür zu verschwenden).Außerdem finde ich auch den Ansatz mit der Vererbung nicht so gut und würde wohl selber am ehesten den in meinem Link verwendeten
[[no_unique_address]] std::conditional_t<Cond, T, std::monostate> m;
benutzen.
-
@john-0 sagte in Member von Strukturen per SFINAE löschen:
@DNKpp sagte in Member von Strukturen per SFINAE löschen:
Das mag wohl sein, trotzdem ist es doch recht umständlich ein Primary Template + zwei Spezialisierungen zu verwenden.
Es ist der Weg, den sich die Macher von C++ ausgedacht haben. Es ist nicht so effizient wie
Es steht doch nirgendwo geschrieben, dass ein Primary Template immer undefiniert sein muss. Es ist ein Fakt, dass es nicht sinnvoll ist, eine Bedingung zu überprüfen und andernfalls die gegenteilige Bedingung nochmal zu prüfen.
Aber mach du nur, wie du willst. Ich hab dir einen alternativen und kürzeren Weg gezeigt. Mehr wollte ich gar nicht.
@Th69 sagte in Member von Strukturen per SFINAE löschen:
Außerdem löschen eure Lösungen doch gar nicht den Member, sondern ihr kopiert einfach die Klasse und fügt in der "non-void"-Klasse den zusätzlichen Member hinzu.
Es geht ja darum bei einer beliebigen Klasse (mit beliebig vielen Membern und Memberfunktionen) einen (oder mehrere) Member zu "löschen" (d.h. keinen Speicherplatz dafür zu verschwenden).Außerdem finde ich auch den Ansatz mit der Vererbung nicht so gut und würde wohl selber am ehesten den in meinem Link verwendeten
[[no_unique_address]] std::conditional_t<Cond, T, std::monostate> m;
benutzen.
Dafür hast du halt hier einen Member, der nichts tut und ggf eher irritiert. Hat doch alles seine Vor- und Nachteile. Zumal das
no_unique_address
Attribut afaik noch gar nicht von allen Compilern supportet wird (msvc bietet z.b.msvc::no_unique_address
als Alternative an).
-
@Th69 sagte in Member von Strukturen per SFINAE löschen:
Außerdem finde ich auch den Ansatz mit der Vererbung nicht so gut und würde wohl selber am ehesten den in meinem Link verwendeten
[[no_unique_address]] std::conditional_t<Cond, T, std::monostate> m;
benutzen.
Ausprobiert?
Ich bekomme mit#include <type_traits> #include <iostream> #include <variant> template <typename T, bool C = std::is_same_v<T, void>> struct Data { int Id = 0; [[no_unique_address]] std::conditional_t<C, T, std::monostate> m; }; int main() { Data<int> d1; // OK Data<void> d2; // OK std::cout << "d1: " << d1.Id << ", " << d1.t << "\n"; std::cout << "d2: " << d2.Id << "\n"; }
folgende Ausgabe (2. Fehler interessiert nicht)
sfinae5.cc: In instantiation of ‘struct Data<void>’: sfinae5.cc:14:13: required from here sfinae5.cc:9:72: error: ‘Data<T, C>::m’ has incomplete type 9 | [[no_unique_address]] std::conditional_t<C, T, std::monostate> m; | ^ sfinae5.cc:9:72: error: invalid use of ‘std::conditional_t<true, void, std::monostate>’ {aka ‘void’} sfinae5.cc: In function ‘int main()’: sfinae5.cc:16:52: error: ‘struct Data<int>’ has no member named ‘t’ 16 | std::cout << "d1: " << d1.Id << ", " << d1.t << "\n"; | ^
-
@john-0 sagte in Member von Strukturen per SFINAE löschen:
std::conditional_t<C, T, std::monostate> m;
Probiers doch mal mit
std::conditional_t<!C, T, std::monostate> m;
-
Manchmal sieht man die trivialsten Dinge nicht. Und es muss an dieser Stelle sein, da es in der Template Namendefinition nicht funktioniert.