Warum gibt's sowas eigentlich nicht als Feature in C++?
-
Shade Of Mine schrieb:
wxSkip schrieb:
OK, kann ich gerne machen. Ich habe mir ein Mittel vorgestellt, mit dem man Code verwenden kann, sofern er funktioniert, und alternativ anderen Code. Die Syntax dafür könnte auch ganz anders aussehen. Das waren jetzt Beispiele, wie ich so etwas verwenden würde.
Und diese Idee ist ansich nur eine Bekämpfung von Symptomen. Warum willst du unterschiedliche map Implementierungen verwenden - doch nur weil deine bevorzugte nicht da ist. Warum unterschiedliche Ctor Aufrufe - doch nur weil die bevorzugte Variante nicht geht.
Das ist ein typischer Fall für introspection/reflection.
Jedes deiner Beispiele lässt sich auch anders lösen - nur wäre Reflection hier sehr nett. Code testweise kompilieren ist nicht unproblematisch - du würdest dir mit deiner Idee eine Menge Probleme einkaufen.
Es ist besser das Problem ansich anzupacken als die Symptome zu bekämpfen. Du weißt nicht ob es unorderd_map gibt -> also teste es vorher. Ideal im Build Script lösbar.
Du willst einen Ctor aufrufen den du nicht genau kennst: perfect forwarding.
Du willst wissen ob eine Funktion so aufrufbar ist: SFINAEEin Trial&Error kompilieren ist einfach eine furchtbar schlechte Idee.
Das mit den Problemen ist für mich verständlich. Ich hätte da aber noch ein paar Verständnisfragen:
1. Unter perfect forwarding habe ich eigentlich verstanden, dass ich rvalues, die an eine Funktion übergeben werden, effizienter oder ohne Geschwindigkeitsverlust durch Rvalue-Referenzen an einen Konstruktor/eine Funktion weitergeben kann. Könntest du da bitte mal ein Beispiel machen, wie du das meinst?2. Okay, SFINAE kannte ich so noch nicht. Würde ich das für Konstruktoren verwenden, müsste ich doch alle wxWidgets-Klassen modifizieren, oder? Außerdem (aus Interesse), was würde passieren, wenn eine Funktion schon als Template existieren würde und ich sie dann überladen würde? Bsp.:
//blah.h, unbekannte Funktionsdeklaration template<typename T> void Increase(T &in); //main.cpp //meine Alternativfunktion, falls es keine Funktion für meinen Aufruf gibt template<typename T> void Increase(T &in) //ERROR: multiple declaration { cout << "SFINAE" << endl; } int main() { int xx = 0; Increase(xx); }
3. Das mit der Bekämpfung von Symptomen verstehe ich nicht ganz:
Wenn ich jetzt keine Tupels mit 15 Typen habe und kein typsicheres printf, ist das dann ein Symptom, wegen dem Variadic Templates eingeführt wurden? Oder ist das ein allgemeines Problem? Wenn ich aus Bequemlichkeit für den User keine verschieden benannten Mathematikfunktionen für verschiedene Zahlentypen einführen will, ist das dann ein Symptom, wegen dem man Funktionsüberladung einführt?4. Was meinst du mit Introspection/Reflexion? Soll ich wxWidgets umschreiben?
-
wxSkip schrieb:
2. Okay, SFINAE kannte ich so noch nicht. Würde ich das für Konstruktoren verwenden, müsste ich doch alle wxWidgets-Klassen modifizieren, oder?
SFINAE hilft dir nicht wirklich weiter, da es sich nur auf die Funktionssignatur bezieht. Aber du hast ja schon erkannt, dass die automatische Auswahl des richtigen Codes in deinem Beispiel nicht funktioniert, warum willst du also krampfhaft versuchen, irgendwelche anderen Sprachmittel dazu hinzubiegen? Alternative Ansätze wurden ja bereits genannt.
wxSkip schrieb:
Außerdem (aus Interesse), was würde passieren, wenn eine Funktion schon als Template existieren würde und ich sie dann überladen würde?
SFINAE heisst nicht Templates überladen. Gerade solche Probleme sollte es unter anderem lösen. Schau dir das Konzept noch etwas genauer an, z.B. bei Wikibooks.
wxSkip schrieb:
Oder ist das ein allgemeines Problem?
Ja, das ist ein allgemeines Problem. Im Gegensatz dazu sehe ich keinen Nutzen in deinem Feature.
wxSkip schrieb:
Wenn ich aus Bequemlichkeit für den User keine verschieden benannten Mathematikfunktionen für verschiedene Zahlentypen einführen will, ist das dann ein Symptom, wegen dem man Funktionsüberladung einführt?
Der Gedanke hinter Funktionsüberladung ist Abstraktion (genau gesagt Polymorphie), nicht Bequemlichkeit.
-
Danke, ich hatte SFINAE wirklich falsch verstanden. Bei meinem Problem hilft mir das aber jetzt nicht weiter, denn 1. ist ein Konstruktor keine Memberfunktion und 2. kann ich alternativ nicht prüfen, ob die Klasse die Memberfunktion Create() hat, oder?
Und was Shade Of Mine mit dem perfect forwarding jetzt gemeint hat, ist mir immer noch nicht klar.
-
Ich verstehe nicht das Problem. Normalerweise kompiliere ich Software und liefere sie aus. Ich uebersetze sie auf meinem System. Wenn ich
std::unordered_set
verwenden moechte, dann stelle ich sicher, dass sie vorhanden ist. Wenn ich eine bestimmte Bibliotheksfunktion benutzen moechte, dann stelle ich sicher, das die Bibliothek auch vorhanden ist. Ist sie es auf dem Zielsystem nicht, so liefere ich sie mit oder der Anwender muss sich selbst drum kuemmern. Das gleiche gilt, wenn der Anwender sie Software selbst uebersetzen moechte. Das gleiche auch fuer die verschiedenen Konstruktoren.Ich verstehe nicht, warum das ein Problem der Programmiersprache C++ ist. Warum ist das ueberhaupt ein Problem?
-
knivil schrieb:
Ich verstehe nicht das Problem. Normalerweise kompiliere ich Software und liefere sie aus. Ich uebersetze sie auf meinem System. Wenn ich
std::unordered_set
verwenden moechte, dann stelle ich sicher, dass sie vorhanden ist. Wenn ich eine bestimmte Bibliotheksfunktion benutzen moechte, dann stelle ich sicher, das die Bibliothek auch vorhanden ist. Ist sie es auf dem Zielsystem nicht, so liefere ich sie mit oder der Anwender muss sich selbst drum kuemmern. Das gleiche gilt, wenn der Anwender sie Software selbst uebersetzen moechte. Das gleiche auch fuer die verschiedenen Konstruktoren.Ich verstehe nicht, warum das ein Problem der Programmiersprache C++ ist. Warum ist das ueberhaupt ein Problem?
Ja, das ist wahrscheinlich meistens kein Problem. Benutze ich Code::Blocks und wechsle lediglich den Compiler auf eine ältere Version, könnte es sein, dass die Library nicht vorhanden ist und das kann ich nicht prüfen, da ich kein eigenes Build Script verwende. Könnte ich machen, weiß aber nicht wie's geht (und da bin ich halt faul ;)). Zum zweiten Problem: Das ist eine Frage der Kapselung - die sich meistens nicht stellt, in meinem Fall aber schon. Könnte man mit mehr Aufwand sicher auch anders lösen. Generell fällt mir im Moment grad kein weiterer Anwendungsfall ein, ich weiß nicht, ob und wie viele es da gäbe.
-
Hm... könnte man mit so etwas nicht auch Concepts ersetzen?
template<typename T> class MyVector { static_assert(not_compile(T() == T()), "Vector type needs an equality operator"); }
-
Schon wieder jemand, der die Macht von Template-Metaprogrammierung unterschätzt.
C++ kann bereits jetzt überprüfen, ob ein Typ den
operator==
unterstützt (das könnte man allerdings noch sauberer lösen):template <typename T> T create(); struct x47 { char x[47]; }; struct equality_comparator { template <typename T> equality_comparator(const T&); }; x47 operator== (equality_comparator, equality_comparator); template <typename T> struct is_equality_comparable { static const bool value = sizeof(x47) != sizeof(create<T>() == create<T>()); };
Anwendung:
#include <iostream> struct not_comparable {}; int main() { std::cout << is_equality_comparable<int>::value << std::endl; std::cout << is_equality_comparable<not_comparable>::value << std::endl; }
-
Das ist ja mal eine Interessante Möglichkeit. Aber ehrlich gesagt versteh ich nicht ganz warum das eigentlich funktioniert.
Könntest du den Code evtl. etwas erläutern Nexus?Gruß Gate
-
Man benutzt Überladungsauflösung, um herauszufinden, ob ein
operator==
existiert:- Falls
T
einen Gleichheitsoperator bereitstellt, werden die beiden Operanden bei
create<T>() == create<T>()
als
T
ausgewertet, das Ergebnis hat höchstwahrscheinlich den Typbool
. Man benutztcreate<T>()
stattT()
, um keinen Defaultkonstruktor zu erzwingen.- Falls nicht, wird die implizite Konvertierung der beiden Operanden zu
equality_comparator
vorgenommen. Der entsprechende überladeneoperator==
gibt einx47
-Objekt zurück.
Nun prüft der
sizeof
-Operator die Grösse des Ausdruckscreate<T>() == create<T>()
. Bei nicht vorhandenemoperator==
hat der Ausdruck die Grösse vonx47
, da zweiequality_comparator
-Objekte verglichen werden. Bei vorhandenem ist der resultierende Typ (meistbool
) mit grösster Wahrscheinlichkeit nicht 47 Bytes gross. Anzumerken ist hierbei, dasssizeof
den Ausdruck nie wirklich auswertet – deshalb genügen auch die Funktionsdeklarationen.Diese kleine Unsicherheit ist ein Teil, den man sauberer lösen könnte. Zudem sollte die ganze Implementierung in einen Namensraum
detail
verfrachtet werden, um Konvertierungen im Benutzercode zu vermeiden. Im Weiteren hat der Ansatz das Problem, dass der Code mitprivate
oderprotected
Member-operator==
nicht kompiliert.
- Falls
-
Hey, super Erklärung vielen Dank dafür.
Zwei Kleinigkeiten noch:
1. Warum hat x47 ein Array von 47 Bytes? Gibt es einen speziellen Grund dafür so einen hohen Wert zu nehmen? In meinem Test hat es auch mit char x[2] beispielsweise geklappt.
2. Ist equality_comparator überhaupt notwending? In meinem Test funktionierte es auch mittemplate <typename T> x47 operator==(T,T)
Dies noch in einen Detail-Namespace verfrachtet wegen versehentlicher Konvertierungen und man sollte auf der sicheren Seite sein.
Oder übersehe ich hier etwas, weswegen equality_comparator notwendig wäre?Gruß Gate
-
Gate schrieb:
1. Warum hat x47 ein Array von 47 Bytes? Gibt es einen speziellen Grund dafür so einen hohen Wert zu nehmen? In meinem Test hat es auch mit char x[2] beispielsweise geklappt.
Das ist ein willkürlicher Wert, der eine relativ kleine Wahrscheinlichkeit hat, dass irgendein Rückgabetyp eines benutzerdefinierten
operator==
s die gleiche Grösse besitzt. Man kann natürlich noch grössere Zahlen nehmen oder es gleich sauber lösen.Gate schrieb:
2. Ist equality_comparator überhaupt notwending?
Beim direkten Funktionstemplate können Probleme entstehen, falls der
operator==
für den TypT
nicht direkt existiert, sondernbool operator==(const U&, const U&);
mit
T
implizit konvertierbar zuU
. Denn sobald ein Funktionstemplate in den Überladungen vorkommt, wird dieses impliziten Konvertierungen vorgezogen. Also würde im Falle einerU
-Überladung angegeben, der TypT
sei nicht equality comparable, obwohl der AusdruckT == T
Sinn macht.Andererseits ist es leider nicht so, dass
equality_comparator
dieses Problem löst. Da zwei Konvertierungen (zuU
und zuequality_comparator
) existieren, liegt eine Mehrdeutigkeit vor. Ich habe versucht, der Klasseequality_comparator
einen Konstruktor mit einer Ellipse zu geben, da diese bei der Überladungsauflösung zuletzt berücksichtigt wird und somit allfällige implizite Konvertierungen bevorzugt würden. Allerdings übersehe ich wohl etwas, die Mehrdeutigkeit existiert nach wie vor...
-
Danke für die Erläuterungen
-
Gate schrieb:
Danke für die Erläuterungen
Von mir auch Echt erstaunlich, was man da alles "rumwurschteln" kann.
-
Na endlich hab ich die Lösung gefunden, um herauszufinden, ob eine Funktion existiert (ich poste dann die vollständige Lösung für das wxWidgets-Problem, sobald ich sie gefunden habe).
#include <string> #include <iostream> using namespace std; struct False_help{char x[5];}; template <typename T> auto SFINAE_has_size(T in) -> decltype(&T::size); False_help SFINAE_has_size(...); #define has_size_func(x) (sizeof(False_help) != sizeof(SFINAE_has_size(x))) int main() { cout << boolalpha << has_size_func(cout) << endl; }
-
Sodala, hier nach langem Herumprobieren hätten wir das Monsterbeispiel:
#include <string> #include <iostream> using namespace std; //test classes: 1 without Create-Function, 1 with Create(int)-Function and 1 with Create(int, string)-Function class Test_1 { bool allright; public: Test_1() : allright(true) {} bool IsGood(){return allright;} }; class Test_2 { bool allright; public: Test_2() : allright(false) {} void Create(int p1, int p2 = 0){allright = true;} bool IsGood(){return allright;} }; class Test_3 { bool allright; public: Test_3() : allright(false) {} void Create(int p1, string p2, int p3 = 0){allright = true;} bool IsGood(){return allright;} }; //SFINAE typedef struct {char x[2];} False_help; typedef char True_help; template<typename T> auto SFINAE_has_create(T *in) -> decltype(&T::Create); False_help SFINAE_has_create(...); template<typename T, typename... Args> True_help SFINAE_is_member_func_pointer(void (T::*)(Args...)); template<typename T> True_help SFINAE_is_member_func_pointer(void (T::*)()); False_help SFINAE_is_member_func_pointer(...); #define has_create_func(x) (sizeof(True_help) == sizeof(SFINAE_is_member_func_pointer(SFINAE_has_create(x)))) template<typename T, typename... Args> True_help SFINAE_create_func_needs_string(void (T::*)(int, string, Args...)); template<typename T, typename... Args> False_help SFINAE_create_func_needs_string(void (T::*)(int, Args...)); #define create_func_needs_string(x) (sizeof(True_help) == sizeof(SFINAE_create_func_needs_string<x>(&x::Create))) template<typename T, bool create_needs_string> struct Call_right_create_func //will be used if create_needs_string == true { static void exec(T *in){in->Create(0, string());} }; template<typename T> struct Call_right_create_func<T, false> { static void exec(T *in){in->Create(0);} }; template<typename T, bool has_create> struct Create_if_true //will be used if has_create == true { static void exec(T *in){Call_right_create_func<T, create_func_needs_string(T)>::exec(in);} }; template<typename T> struct Create_if_true<T, false> { static void exec(T *in){} }; template<typename T> T *create_class() { T *tmp = new T(); Create_if_true<T, has_create_func(tmp)>::exec(tmp); //if the type has a Create()-function, it will be called return tmp; } int main() { Test_1 *x1 = create_class<Test_1>(); Test_2 *x2 = create_class<Test_2>(); Test_3 *x3 = create_class<Test_3>(); cout << boolalpha << x1->IsGood() << endl << x2->IsGood() << endl << x3->IsGood() << endl; }
Und hier die Library-Version, wie sie mit wxWidgets funktionieren sollte (ungetestet, aber fast das gleiche wie oben, und das ist getestet):
//SFINAE typedef struct {char x[2];} False_help; typedef char True_help; template<typename T> auto SFINAE_has_create(T *in) -> decltype(&T::Create); False_help SFINAE_has_create(...); template<typename T, typename... Args> True_help SFINAE_is_member_func_pointer(void (T::*)(Args...)); template<typename T> True_help SFINAE_is_member_func_pointer(void (T::*)()); False_help SFINAE_is_member_func_pointer(...); #define has_create_func(x) (sizeof(True_help) == sizeof(SFINAE_is_member_func_pointer(SFINAE_has_create(x)))) template<typename T, typename... Args> True_help SFINAE_create_func_needs_string(void (T::*)(wxWindow *, int, wxString, Args...)); template<typename T, typename... Args> False_help SFINAE_create_func_needs_string(void (T::*)(wxWindow *, int, Args...)); #define create_func_needs_string(x) (sizeof(True_help) == sizeof(SFINAE_create_func_needs_string<x>(&x::Create))) template<typename T, bool create_needs_string> struct Call_right_create_func //will be used if create_needs_string == true { static void exec(T *in){in->Create(NULL, wxID_ANY, wxEmptyString);} }; template<typename T> struct Call_right_create_func<T, false> { static void exec(T *in){in->Create(NULL, wxID_ANY);} }; template<typename T, bool has_create> struct Create_if_true //will be used if has_create == true { static void exec(T *in){Call_right_create_func<T, create_func_needs_string(T)>::exec(in);} }; template<typename T> struct Create_if_true<T, false> { static void exec(T *in){} }; template<typename T> T *create_class() { T *tmp = new T(); Create_if_true<T, has_create_func(tmp)>::exec(tmp); //if the type has a Create()-function, it will be called return tmp; }
-
Nächstes Mal bin ich vorsichtiger, wenn ich die Möglichkeiten von C++ aufzeigen will. Ob dieses Template-Gefrickel wirklich der richtige Weg ist, bleibt nämlich nach wie vor fragwürdig. Oder anders gesagt: Nur weil etwas möglich ist, ist es nicht sinnvoll.
Im Normalfall ist es ein Anzeichen schlechten Designs, zuerst das Vorhandensein einer Funktionalität abzufragen und dann abhängig von ihr darauf zu reagieren. Zumindest im grösseren Rahmen sollten solche expliziten Fallunterscheidungen vermieden werden. Und zwar aus ähnlichen Gründen wie virtuelle Funktionen einer
if-else
-Kaskade vorzuziehen sind.Im konkreten Fall hier versuchst du etwas nachzubauen, das wxWidgets bewusst nicht anbietet. Es ist nämlich eine wesentliche Designentscheidung, wenn Klassen keine Defaultkonstruktoren besitzen. Diese deutet darauf hin, dass bei der Konstruktion von Objekten bereits genügend Informationen vorhanden sein müssen, um eine sinnvolle Initialisierung vorzunehmen. Mit deinem Ansatz arbeitest du an diesem Design vorbei und erzeugst Dummy-Objekte, die du nachträglich füllen musst. Warum? Sowas ist nie gut.
-
Nexus schrieb:
Nächstes Mal bin ich vorsichtiger, wenn ich die Möglichkeiten von C++ aufzeigen will. Ob dieses Template-Gefrickel wirklich der richtige Weg ist, bleibt nämlich nach wie vor fragwürdig. Oder anders gesagt: Nur weil etwas möglich ist, ist es nicht sinnvoll.
Im Normalfall ist es ein Anzeichen schlechten Designs, zuerst das Vorhandensein einer Funktionalität abzufragen und dann abhängig von ihr darauf zu reagieren. Zumindest im grösseren Rahmen sollten solche expliziten Fallunterscheidungen vermieden werden. Und zwar aus ähnlichen Gründen wie virtuelle Funktionen einer
if-else
-Kaskade vorzuziehen sind.Im konkreten Fall hier versuchst du etwas nachzubauen, das wxWidgets bewusst nicht anbietet. Es ist nämlich eine wesentliche Designentscheidung, wenn Klassen keine Defaultkonstruktoren besitzen. Diese deutet darauf hin, dass bei der Konstruktion von Objekten bereits genügend Informationen vorhanden sein müssen, um eine sinnvolle Initialisierung vorzunehmen. Mit deinem Ansatz arbeitest du an diesem Design vorbei und erzeugst Dummy-Objekte, die du nachträglich füllen musst. Warum? Sowas ist nie gut.
Ich wollte eigentlich
1.So eine Art Feeling wie beispielsweise bei Java (da hab ich zb SWT verwendet) zu ermöglichen. Mir ist schon klar, dass die so erstellten Objekte gar nicht angezeigt werden. Bei SWT kann man beispielsweise einfach das Objekt erstellen und danach die Parameter setzen, die man möchte. (ist das bei Qt nicht auch so?)
2. Vektoren für Pointer erstellen, die ihre Member von selbst Initialisieren (und zwar gescheit, dass man damit arbeiten kann, und eben nicht nur Default-Initialisiert).
-
wxSkip schrieb:
1.So eine Art Feeling wie beispielsweise bei Java (da hab ich zb SWT verwendet) zu ermöglichen. Mir ist schon klar, dass die so erstellten Objekte gar nicht angezeigt werden. Bei SWT kann man beispielsweise einfach das Objekt erstellen und danach die Parameter setzen, die man möchte. (ist das bei Qt nicht auch so?)
Warum Objekte nicht sinnvoll initialisieren, sondern Parameter erst im Nachhinein setzen? Das öffnet lediglich Fehlerquellen und umgeht das Konzept der Konstruktoren. Dass andere Bibliotheken oder Sprachen sich diesbezüglich anders verhalten, ist doch kein Argument. Java vererbt auch an Orten, wo es keinen Sinn macht.
wxSkip schrieb:
2. Vektoren für Pointer erstellen, die ihre Member von selbst Initialisieren (und zwar gescheit, dass man damit arbeiten kann, und eben nicht nur Default-Initialisiert).
myVector.push_back(/* (Zeiger auf) vollständig initialisiertes Objekt */)
Ich seh das Problem nicht... Die gewünschte Selbst-Initialisierung liefert dir auch nur wieder ein Dummy-Objekt, das du noch bearbeiten musst.
-
Nexus schrieb:
wxSkip schrieb:
2. Vektoren für Pointer erstellen, die ihre Member von selbst Initialisieren (und zwar gescheit, dass man damit arbeiten kann, und eben nicht nur Default-Initialisiert).
myVector.push_back(/* (Zeiger auf) vollständig initialisiertes Objekt */)
Ich seh das Problem nicht... Die gewünschte Selbst-Initialisierung liefert dir auch nur wieder ein Dummy-Objekt, das du noch bearbeiten musst.
myVector.resize(15);
Ich versuche gerade, eine Art Compiler zu schreiben für eine Sprache, in der das auch ohne Konstruktoren gehen soll (nicht nach dem Motto "was musste ich da jetzt schon wieder und in welcher Reihenfolge angeben und warum?"). Da wäre es für mich eine Erleichterung, das direkt so nach C++ übertragen zu können.
-
wxSkip schrieb:
myVector.resize(15);
Ja, das ist mir schon klar. Ich kann auch nicht
std::sort()
auf Listen anwenden. Weil beide Funktionen eben Voraussetzungen haben.resize()
erfordert einen Standardkonstruktor,sort()
Random-Access-Iteratoren.Aber darum geht es sowieso nicht. Ich habe ja erklärt, warum dein Vorgehen Nachteile mit sich bringt und wieso es im Prinzip ein Rückschritt in C++ ist, wo Kapselung und damit auch Klasseninvarianten einen hohen Stellenwert haben.
wxSkip schrieb:
nicht nach dem Motto "was musste ich da jetzt schon wieder und in welcher Reihenfolge angeben und warum?
Das ist natürlich ein guter Grund. Nur ist die Frage, ob er die Nachteile wert ist? Du kannst zum Beispiel auch keine temporären Objekte mehr an Funktionen übergeben.
Bei einem eigenen Projekt ist es auch wieder was Anderes, dann könntest du so ein Design wenigstens konsistent durchziehen. Aber bei einer bestehenden Bibliothek würde ich mir solche Eingriffe gut überlegen, da sie wie schon erwähnt Designüberlegungen des Entwicklers ausser Kraft setzen. Es gibt ja auch andere Möglichkeiten wie das Named Constructor Idiom, das zumindest in die Richtung geht.
Aber ich denke, generell bist du in C++ schlecht aufgehoben, wenn du Funktionsargumente beim Aufruf unbedingt benennen willst. Auch wenn es Möglichkeiten wie Boost.Parameter gibt, sind diese etwas zwiespältig.