Template--Parameter bei Aufruf des Elternkonstruktors in C++
-
Hiho,
ich stelle grad ein Projekt von C++17 auf C++20 um (in aktuellen VS 17.4.0) und habe zu einer Verhaltensänderung eine Frage.
template<typename T, typename Distinct> class Derived_exception_type_dist : public Exception<T> { public: Derived_exception_type_dist(const Exception<T>::String_type& error = Exception<T>::String_type()) : Exception(error) { } };
Was im gekürzten Codebeispiel zu sehen ist, hatte im VS mit C++17 bisher immer funktioniert. Jetzt gibt es einen Compilefehler, weil er 'Exception(error)' nicht mehr mag.
Ich bekomme es kompiliert, wenn ich den Template-Parameter mit hinschreibe ('Exception <T> (error)').
I. d. R. vereinfachen ja die neueren Standards die Sachen und machen es nicht komplizierter.
Warum muss ich jetzt den Template-Parameter explizit hinschreiben, wo es vorher auch ohne ging?VG
Pellaeon
-
Um das etwas nachvollziehbarer zu machen:
MSVC: C++20, C++17
GCC: C++20, C++17
Clang: C++20, C++17Ich denke, das war wohl eher eine Eigenheit des MSVC C++17-Compilers und ich wüsste auch auf Anhieb nicht, welche Neuerung in C++20 dafür verantwortlich sein sollte. Ausser dass der Typ
Exception<T>::String_type
in C++17 eigentlich noch eintypename
davor haben müsste (da gibt es tatsächlich eine Neuerung, dass das an vielen Stellen nicht mehr benötigt wird) - was MSVC im C++17 aber anscheinend ebenfalls nicht haben will, während die anderen meckern.Was allerdings geht, ist beim Klassennamen selbst innerhalb der Klasse die Template-Argumente wegzulassen, also z.B. für einen Delegating Constructor, eine Member-Variable, Funktionsparameter, etc. Bei der Basisklasse war mir das aber bisher nicht bekannt.
-
@Pellaeon sagte in Template--Parameter bei Aufruf des Elternkonstruktors in C++:
Warum muss ich jetzt den Template-Parameter explizit hinschreiben, wo es vorher auch ohne ging?
Weil Microsoft hin und wieder auch mal Bugs in ihrem Compiler ausbessert.
-
@Finnegan @hustbaer vielen Dank. Das kann natürlich gut sein, dann liegts wahrscheinlich an einer MSVC-Eigenheit.
Grad ist noch ein anderes Problem aufgetreten. Bevor ich jetzt für dafür wieder einen neuen Thread aufmache, poste ich es gleich mal hier drinnen:
Ich habe mehrere Methoden "get_oobb" für verschiedene Typen. Das ganze wird an anderer Stelle mit Templates genutzt. Wenn ein Typ reinkommt, für den keine Code verfügbar ist, soll eine lesbare Fehlermeldung erscheinen. Das habe ich bisher so gelöst (Auszug):
inline Oriented_box get_oobb(const Cylinder& cyl) { const auto& cyl_dir = cyl.axis.direction; [...] } // inline Oriented_box get_oobb(const Cone& cone) { const auto& cone_dir = cone.ray.direction; [...] } //only for type deduction template<typename T> inline T get_oobb(const T& obj) { static_assert(false, "get_oobb not supported for this type"); return obj; }
Ich habe für bekannte Typen freie inline-Funktionen und für alles andere ein Template mit einem static_assert, was die von mir gewünschte Fehlermeldung schmeißt.
Jetzt habe ich eine cpp-Datei, welche die Headerdatei, wo das mit drinnen ist, inkludiert. Die Methode selbst wird da garnicht genutzt. Ich mache nun strg+F7, um die Einzeldatei zu kompilieren und erhalte die Fehlermeldung aus dem static (1>D:\libraries\cpp\matrem\matrem 0.9\include\Basic_types_gte.h(202,17): error C2338: static_assert failed: 'get_oobb not supported for this type')
Mit C++17 dagegen funktioniert es. Ist das wieder eine Eigenheit vom MSVC und hätte an sich ursprünglich shcon kompilieren dürfen? Wo liegt das Problem?
VG & Danke
Pellaeon
-
Hallo,
interessante Frage...hab dazu nichts gefunden. Mit
template<typename T> inline T get_oobb(const T& obj) { static_assert(not (std::is_same_v<T,T>), "get_oobb not supported for this type"); return obj; }
scheint es wie gewollt zu funktionieren -mit Betonung auf "scheint". Keine Ahnung, ob das garantiert ist.
-
@Jockelx Es kann natürlich auch sein, dass es mit dem Code da garnichts zu tun hat, und durch das Inkludieren anderer Teile das Problem erst zustande kommt. Aber das gepostete ist zumindest der Part, wo das (falsche) Auslösen des static_assert drinnen steht.
-
-
Hm, dann muss ich mir wohl erst einmal einen anderen Mechanismus überlegen. Genutzt hatte ich das ganze für folgenden Aufruf
//if an Object_with_oob type is available, this type is returned, otherwise the given object will be returned template<typename Type_t> inline decltype(auto) get_with_optional_oobb(Type_t&& obj) { if constexpr (std::is_same_v<Oriented_box, decltype(get_oobb(obj))>) return make_with_oobb(std::forward<Type_t>(obj)); else return std::forward<Type_t>(obj); }
Ich habe verschiedene math. Objekte, für manche gibts bisher eine Bounding-Box-Berechnung, für andere noch nicht. Mit dem "get_with_optional_oobb" kann (C++20: konnte) man sich die Optimierung reinsetzen lassen, falls vorhanden. Das decltype hat auf den Rückgabetyp von get_oobb geschaut. Wenn keine exisiert, kam der eigene Tyo zurück und "make_with_oobb" wurde nicht aufgerufen. Da hat das static_assert auch nicht gezündet. Jetzt passiert halt irgendwie was anders.
Ich habe aber auch grad eine andere Stelle, da steht vor einerTemplate-Funktion "inline", was zu einem internen Compilerfehler führt. Nehme ich das inline weg, geht es durch. Schöne neue Welt ... .
geht template<typename Exception_t, typename Cont, typename Id> typename const Cont::value_type& get_container_value_by_id(const Cont& con, const Id& id, const mocol::char_t* p_ex_msg, unsigned short err_code = 0) { return *get_container_value_iter_id<Exception_t>(con, id, p_ex_msg, err_code); } geht nicht template<typename Exception_t, typename Cont, typename Id> inline typename const Cont::value_type& get_container_value_by_id(const Cont& con, const Id& id, const mocol::char_t* p_ex_msg, unsigned short err_code = 0) { return *get_container_value_iter_id<Exception_t>(con, id, p_ex_msg, err_code); }
-
@Jockelx sagte in Template--Parameter bei Aufruf des Elternkonstruktors in C++:
Hallo,
interessante Frage...hab dazu nichts gefunden. Mit
template<typename T> inline T get_oobb(const T& obj) { static_assert(not (std::is_same_v<T,T>), "get_oobb not supported for this type"); return obj; }
scheint es wie gewollt zu funktionieren -mit Betonung auf "scheint". Keine Ahnung, ob das garantiert ist.
Ja, das müsste passen. AFAIK ist es laut Standard erlaubt dass
static_assert
die nicht von Template-Parametern abhängig sind gleich beim Parsen des Templates geprüft werden - und nicht erst bei der Instanzierung. Eine formale Abhängigkeit wie hier, also eine die sich eigentlich "rausoptimieren" liesse, sollte reichen.
!sizeof T
sollte auch reichen.Eine Alternative wäre sonst noch mehrere unabhängige Overloads zu machen. Also praktisch einfach das Haupt-Template weglassen. Dann sollte der Compiler in der Fehlermeldung darauf hinweisen dass keiner der verfügbaren Overloads in Frage kommt.
-
@hustbaer Kommentiere ich das static_assert aus, kompiliert er durch. Nehme ich das ganze Template raus, gibt es einen Compilefehler bei "if constexpr (std::is_same_v<Oriented_box, decltype(get_oobb(obj))>)" (error C2665: "matrem::get_oobb": Keine überladene Funktion konnte alle Argumenttypen konvertieren (Quelldatei wird kompiliert Matrem_manager.cpp)). Also ohne die Generalisierung geht es nicht, zumindest nicht so, wie es aktuell aufgebaut ist.
VG
-
@hustbaer Dein Hinweis ging mir eben noch einmal durch den Kopf, jetzt habe ich verstanden was du meintest Damit funktioniert es wieder:
static_assert(sizeof(T) != sizeof(T), "get_oobb not supported for this type");
-
@Pellaeon sagte in Template--Parameter bei Aufruf des Elternkonstruktors in C++:
Nehme ich das ganze Template raus, gibt es einen Compilefehler bei "if constexpr (std::is_same_v<Oriented_box, decltype(get_oobb(obj))>)" (error C2665: "matrem::get_oobb": Keine überladene Funktion konnte alle Argumenttypen konvertieren (Quelldatei wird kompiliert Matrem_manager.cpp)). Also ohne die Generalisierung geht es nicht, zumindest nicht so, wie es aktuell aufgebaut ist.
IMHO ist das nur eine etwas verklausuliertere Art zu sagen "get_oobb not supported for this type". Man könnte durchaus überlegen, ob so eine Fehlermeldung nicht eventuell ausreichend ist und sich den zusätzlichen Code sparen. Wenn ich mir unbekannten Code kompiliere, weiss ich persönlich bei einer solchen Fehlermeldung jedenfalls schneller, was das Problem ist, als wenn ich erst noch herausfinden müsste, weshalb dieses
static_assert
jetzt feuert. Dann sehe ich noch Dinge wiesizeof(T) != sizeof(T)
und kratze mich erstmal mächtig am Kopf.Verständlichere Fehlermeldungen zu genereieren ist gut gemeint, aber wenn dafür irgendwann zu unintuitive Konstrukte benötigt werden, könnte das den Code schwerer zu verstehen machen und letztendlich die Wartungskosten erhöhen. Nur so eine Anregung.
Ich würde hier jedenfalls mit der Standard-Meldung des Compilers gehen - es sei denn ich brauche aus irgendeinem Grund eine eindeutige, compilerunabhängige Fehlermeldung. Z.B. weil diese in einem Build-Skript geparst werden muss um dann irgendwelche Aktionen durchzuführen.
-
@Finnegan Ich benötige das Template, damit das "if constexpr" in meiner gezeigten Methode weiter oben funktioniert. Anhand des Rückgabetyps von get_oobb unterscheide ich, ob es eine valide Methode gibt oder nicht. Daher brauche ich einen Ausdruck für alle Typen, die keine oobb anbieten. Dieser Ausdruck soll aber natürlich nicht normal benutzbar sein, weil das sinnfrei wäre. Daher das assert, was dann einen Compilefehler verursacht.
Ohne das Template geht einfach das if constexpr an der Stelle nicht.VG
-
@Pellaeon sagte in Template--Parameter bei Aufruf des Elternkonstruktors in C++:
@Finnegan Ich benötige das Template, damit das "if constexpr" in meiner gezeigten Methode weiter oben funktioniert. Anhand des Rückgabetyps von get_oobb unterscheide ich, ob es eine valide Methode gibt oder nicht. Daher brauche ich einen Ausdruck für alle Typen, die keine oobb anbieten. Dieser Ausdruck soll aber natürlich nicht normal benutzbar sein, weil das sinnfrei wäre. Daher das assert, was dann einen Compilefehler verursacht.
Ohne das Template geht einfach das if constexpr an der Stelle nicht.Ah, verstehe. Wobei hier auch lediglich eine Deklaration ohne Implementierung reichen sollte, was dann bei versehentlicher Verwendung auf einen Linker-Fehler laufen würde. Eventuell lässt sich dieses "Fallthrough"-Template auch in einen unbenannten Namespace packen, so dass es nur vom
if constexpr
-Ausdruck aufgegriffen werden kann, nicht aber von irgendwelchem anderen Code.Ich sage nicht, dass das so wie es jetzt ist eine schlechte Lösung ist, ich tendiere nur gerne zu möglichst simplen Lösungen.
Vielleicht noch eine alternative Idee - wenn schon C++20, dann kann man das eventuell mit Concepts noch etwas einfacher lösen:
#include <iostream> #include <concepts> struct Oriented_box {}; struct Cylinder {}; struct Cone {}; Oriented_box get_oobb(const Cylinder& cyl); Oriented_box get_oobb(const Cone& cone); template <typename T> concept supports_get_oobb = requires(T a) { { get_oobb(a) } -> std::same_as<Oriented_box>; }; int main() { if constexpr (supports_get_oobb<Cylinder>) std::cout << "Cylinder supports get_oobb\n"; if constexpr (supports_get_oobb<Cone>) std::cout << "Cone supports get_oobb\n"; if constexpr (supports_get_oobb<Oriented_box>) std::cout << "Oriented_box supports get_oobb\n"; if constexpr (supports_get_oobb<int>) std::cout << "int supports get_oobb\n"; }
Output:
Cylinder supports get_oobb Cone supports get_oobb
Hat den Vorteil, dass man die Template-Funktion nicht mehr braucht (vermeidet versehentliche Verwendung) und dass der Ausdruck im
if constexpr
etwas "sprechender" ist (leichter zu verstehen für andere).Edit:
Das ginge sogar noch kompakter, auch ohne ein eigenes Concept dafür einzuführen - wenn man es z.B. nur an dieser einen Stelle im Code benötigt:
//if an Object_with_oob type is available, this type is returned, otherwise the given object will be returned template<typename Type_t> inline decltype(auto) get_with_optional_oobb(Type_t&& obj) { if constexpr ( requires { { get_oobb(obj) } -> std::same_as<Oriented_box>; } ) return make_with_oobb(std::forward<Type_t>(obj)); else return std::forward<Type_t>(obj); }
Siehe:
Requires expression: Yields a prvalue expression of type bool that describes the constraints.
Ich weiss nicht, ob
requires
-Ausdrücke dafür gedacht waren, die so zu verwenden, aber es funktioniert und ist m.E. recht gut verständlich
-
@Pellaeon sagte in Template--Parameter bei Aufruf des Elternkonstruktors in C++:
@Finnegan Ich benötige das Template, damit das "if constexpr" in meiner gezeigten Methode weiter oben funktioniert. Anhand des Rückgabetyps von get_oobb unterscheide ich, ob es eine valide Methode gibt oder nicht.
Du könntest auch einfach den Rückgabetyp des Templates auf
void
ändern. Das verhindert zumindest schonmal sinnvollen Verwendungen mit Typen für die es keine Spezialisierung gibt. Also einfach nurget_oobb(foo);
könnte schon noch jmd. schreiben und es würde kompilieren. Aberauto var = get_oobb(foo);
ginge schon nicht mehr, weil das Ding ja nix zurückgibt.Ansonsten mache ich sowas gerne auch mit Klassen-Templates. Also ein Klassen-Template wo in den spezialisierungen dann ne statische Funktion drinnen ist. Test ob es eine Spezialisierung gibt kann man dann z.B. machen indem man das (ansonsten leere) Haupt-Template von einer Hilfsklasse ableitet.
Und für die Convenience kann man sich ein kleines Funktions-Template schreiben das einfach nur die statische Funktion im Klassen-Tempalte aufruft.
-
Dieser Beitrag wurde gelöscht!
-
@hustbaer Stimmt, das mit dem void ist auch eine gute Idee. Wobei ich in dem Fall das static_assert nicht so schlimm und immer noch verständlich finde. Die concept-Lösung ist natürlich auch ein guter Ansatz. Den werde ich evtl. reinnehmen, einfach um so langsam auf C++20 umzustellen.
@Finnegan Zu deiner concept-Lösung hätte ich eine Frage. Die bezieht sich aber auf eine andere Code-Stelle in meinem Programm. Ich strippe das mal auf das nötigste runter.
Und zwar nutze ich die GTEngine, um Objekte auf Kollisionen zu testen. Dort gibt es eine Template-Strukur "TIQuery", für diese ruft man den operator() auf. Als Ergebnis wird eine Struktur geliefert, welche die bool-Variable intersect hat und mir sagt, ob eine Kollision vorliegt oder nicht.
Beim operator ist die Reihenfolge wichtig, also z. B. query(cyl, sphere) geht, query(sphere, cyl) ist ein Fehler.
Ich habe mir nun mit C++17 einen Mechanismus gebastelt, sodass der Aufruf der Reihenfolge egal ist:// template<typename Type_t> inline bool is_collision(const Type_t& left, const Type_t& right) { gte::TIQuery<Real_t, Type_t, Type_t> query; return query(left, right).intersect; } // template<typename Left_t, typename Right_t> inline auto is_collision(const Left_t& left, const Right_t& right) -> decltype(gte::TIQuery<Real_t, Left_t, Right_t>::Result::intersect) { if (has_points_inside(left, right)) return true; gte::TIQuery<Real_t, Left_t, Right_t> query; return query(left, right).intersect; } // template<typename Left_t, typename Right_t> inline auto is_collision(const Left_t& left, const Right_t& right) -> decltype(gte::TIQuery<Real_t, Right_t, Left_t>::Result::intersect) { if (has_points_inside(left, right)) return true; gte::TIQuery<Real_t, Right_t, Left_t> query; return query(right, left).intersect; } // template<typename Left_t, typename Right_t> inline bool is_collision(const Left_t& left, const Object_with_oobb<Right_t>& right) { //if the bounding box test fails, we can spare the actual object intersection test if (!is_collision(left, right.bounding_obj)) return false; // return is_collision(left, right.obj); }
Die Rückgabetypen-Herleitung musste ich teilweise unterschiedlich machen, damit es keine Mehrdeutigkeit gibt, auch wenn es faktisch immer am Ende bool ist. So wie es da steht, kompiliert das und funktioniert bei mir im VS. Die erste Funktion ist der Sonderfall, dass beide Typ gleich sind. Das brauche ich, da ich ansonsten bei den beiden allgemeinen varianten eine Mehrdeutigkeit habe, da ja beide gehen würden.
Ich habe nun versucht, dass mit concepts zu lösen:
// template<typename Left_t, typename Right_t> concept supports_left_right = requires(Left_t left, Right_t right) { { gte::TIQuery<Real_t, Left_t, Right_t>()(left, right).intersect } -> std::same_as<bool>; }; // template<typename Left_t, typename Right_t> concept supports_right_left = requires(Left_t left, Right_t right) { { gte::TIQuery<Real_t, Right_t, Left_t>()(right, left).intersect } -> std::same_as<bool>; }; // template<typename Left_t, typename Right_t> inline bool is_collision(const Left_t& left, const Right_t& right) { if (has_points_inside(left, right)) return true; if constexpr (supports_left_right<Left_t, Right_t>) { gte::TIQuery<Real_t, Left_t, Right_t> query; return query(left, right).intersect; } else if constexpr (supports_right_left<Left_t, Right_t>) { gte::TIQuery<Real_t, Right_t, Left_t> query; return query(right, left).intersect; } else { static_assert(false, "hier stimmt was nicht"); } }
Nach meinem Verständnis stimmt die Abfrage im concept. Ich habe den Typ mit den Templateparametern, ich instanziiere durch das (), dann rufe ich operator() und werte dann das Ergebnis aus, was ein bool sein muss.
Wenn ich das kompiliere, geht das durch. Jedoch landet er immer im assert. Kein concept greift. Wo liegt mein Fehler?VG
-
@Pellaeon sagte in Template--Parameter bei Aufruf des Elternkonstruktors in C++:
template<typename Left_t, typename Right_t> concept supports_right_left = requires(Left_t left, Right_t right) { { gte::TIQuery<Real_t, Right_t, Left_t>()(right, left).intersect } -> std::same_as<bool>; };
Whoa! Gleich in die Vollen, was? Ich muss zugeben, dass ich mich noch nicht so trittsicher mit Concepts fühle, dass ich gedacht hätte, das sowas funktioniert, weil der Zugriff auf die Member-Variable eines Rückgabewerts doch zu sehr nach einer Runtime-Auswertung aussieht, die man in den Requirements natürlich nicht machen kann. Aber es stimmt, man kann natürlich auch Compile-Time-Aussagen über den Typ dieses Members machen - ich hätte nur nicht gedacht, dass man es so wild treiben kann
Ich denke ich weiss, warum der Constaint hier nicht erfüllt wird: Der Rückgabetyp von
gte::TIQuery<Real_t, Right_t, Left_t>()(right, left)
ist eine temporärer (p)rvalue, wodurch sein nicht-statischer Memberintersect
ebenfalls ein rvalue (xvalue in dem Fall wenn ich mich nicht irre) ist. Der exakte Typ dieses Ausdrucks ist also nichtbool
sondernbool&&
.Ich habe dein Beispiel noch etwas weiter eingedampft, so dass es auch isoliert kompilierbar ist:
#include <concepts> #include <iostream> struct A { bool intersect; }; struct B { int intersect; }; auto query(int, int) { return A{ true }; } auto query(bool, bool) { return B{ 0 }; } template <typename T0, typename T1> auto query(T0, T1) { return false; } template <typename T0, typename T1> struct Query { auto operator()(T0 left, T1 right) { return query(left, right); } }; template<typename Left_t, typename Right_t> concept supports_left_right = requires(Left_t left, Right_t right) { { Query<Left_t, Right_t>()(left, right).intersect } -> std::same_as<bool&&>; }; int main() { if constexpr (supports_left_right<int, int>) std::cout << "(int, int) supports left-right\n"; if constexpr (supports_left_right<bool, bool>) std::cout << "(bool, bool) supports left-right\n"; if constexpr (supports_left_right<int, bool>) std::cout << "(int, bool) supports left-right"; }
Output:
(int, int) supports left-right
Interessant ist, dass eigentlich auch
get_oobb(a)
aus meinem vorherigen Beispiel einOriented_box&&
ist, dasstd::same_as<Oriented_box>
aber dennoch greift. Da müsste ich aber noch etwas mehr nachlesen, um genau sagen zu können, warum das hier jetzt nicht funktioniert.Es macht vielleicht Sinn, die Requirements nicht zu restriktiv zu formulieren. Vielleicht reicht es ja, dass der Rückgabetyp überhaupt nur einen
intersect
-Member hat:requires(Left_t left, Right_t right) = { Query<Left_t, Right_t>()(left, right).intersect };
oder dass der Member implizit in einen
bool
konvertierbar ist:requires(Left_t left, Right_t right) { { Query<Left_t, Right_t>()(left, right).intersect } -> std::convertible_to<bool>; };
oder du prüfst, ob
Query<Left_t, Right_t>()(left, right)
einen bekannten Typen zurückgibt, aus dem du schliessen kannst, dass diese Operation unterstützt wird.
-
Ansonsten auch noch ne Frage an die anderen hier: Ich glaube das liegt hier nicht 100% auf Linie mit der ursprünglichen Intention von Concepts, aber es erlaubt eine Menge Tests simpler und ohne schwer zu verstehende SFINAE-Template-Konstrukte zu formulieren.
Was haltet ihr von so einem Ansatz?
-
@Finnegan Funktioniert alles nicht. Ich rätsel noch, warum es nicht geht. Ich habe das Beispiel mal in kleiner Form hier untergebracht: https://godbolt.org/z/afGrb1Mf7 und dort auch mal den GCC genutzt, selbes Verhalten.
-
Ok, also das Concept funktioniert vom Prinzip her so wie oben geschrieben. Das Problem ist einfach, dass es eine Art zeitige Auswertung des static_assert zu geben scheint. Lasse ich das weg, funktioniert es:
// template<typename Left_t, typename Right_t> concept supports_left_right = requires(Left_t left, Right_t right) { gte::TIQuery<Real_t, Left_t, Right_t>()(left, right).intersect; }; // template<typename Left_t, typename Right_t> inline bool is_collision(const Left_t& left, const Right_t& right) { if (has_points_inside(left, right)) return true; if constexpr (supports_left_right<Left_t, Right_t>) { gte::TIQuery<Real_t, Left_t, Right_t> query; return query(left, right).intersect; } else { gte::TIQuery<Real_t, Right_t, Left_t> query; return query(right, left).intersect; } }