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



  • @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