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.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; } }
-
@Pellaeon Die Sache mit dem
static_assert
wurde hier im Thread schonmal angesprochen. Siehe dazu auch diese Bemerkungen zu constexpr if:Note: the discarded statement can't be ill-formed for every possible specialization:
template<typename T> void f() { if constexpr (std::is_arithmetic_v<T>) // ... else static_assert(false, "Must be arithmetic"); // ill-formed: invalid for every T }
The common workaround for such a catch-all statement is a type-dependent expression that is always false:
template<class> inline constexpr bool dependent_false_v = false; template<typename T> void f() { if constexpr (std::is_arithmetic_v<T>) // ... else static_assert(dependent_false_v<T>, "Must be arithmetic"); // ok }
Der
static_assert
-Ausdruck muss vom Template-Parameter abhängig sein, damit es nicht ausgewertet wird.
-
@Finnegan stiiiiimmt. Das ist halt der Unterschied zw. Kurz- und Langzeitgedächtnis Danke für die Erinnerung! Faktisch klappt es ja aber auch mit der verkürzten Variante. Entweder <left, right> geht, dann geht er da rein, ansonsten probiert er es hart im else-Zweig. Und wenn das auch nicht geht. Ist das ja dann auch ein Compilefehler.
-
@Pellaeon sagte in Template--Parameter bei Aufruf des Elternkonstruktors in C++:
Ist das ja dann auch ein Compilefehler.
Die würde ich auch immer bevorzugen, sofern sie verständlich genug sind (ist ja in den letzten Jahren deutlich besser geworden) und die Alternative lauten würde, den Code zu verkomplizieren (Wartungskosten).