Template--Parameter bei Aufruf des Elternkonstruktors in C++



  • @Pellaeon Nee, das hat schon mit dem 17 auf 20 wechsel zu tun. Kannst du ja bei Godbolt gut testen.



  • 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 wie sizeof(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 nur get_oobb(foo); könnte schon noch jmd. schreiben und es würde kompilieren. Aber auto 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 Member intersect ebenfalls ein rvalue (xvalue in dem Fall wenn ich mich nicht irre) ist. Der exakte Typ dieses Ausdrucks ist also nicht bool sondern bool&&.

    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 ein Oriented_box&& ist, das std::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).


Anmelden zum Antworten