Min und max für zwei Template Argumente, constexpr



  • In der STL ist max z.B. folgendermaßen definiert:

    template <class T> constexpr const T& max (const T& a, const T& b);
    

    Was aber nun, wenn ich ein max((char)1, (int)-573) bestimmen möchte?
    Dann sind die Template Argumente von verschiedenen Typen und man kommt nicht weiter.

    Daher hab ich mir folgendes überlegt:

    template <class T1, class T2>
    constexpr auto max(const T1 &a, const T2 &b) -> decltype(a<b?b:a){
       return a<b?b:a;
    }
    

    Kann das funktionieren?



  • Ich nehme an, dass der Compiler etwas dazu sagt.

    Welchen Zweck soll das Template denn haben? 1 und -375 wirst du ja auch so hinkriegen ...



  • In c++ definiert

    const typ name;

    tatsächlich eine compile-time Konstante im Gegensatz zu c.
    Die definierten Konstanten können natürlich sowohl negativ als auch positiv sein.
    Jetzt kann es allgemein natürlich vorkommen, dass man das Max/Min einer Konstanten und der Summe zweier anderer zur Kompilierzeit bestimmen möchte.
    Je nach Vorzeichen und Wert der Konstanten kann sich ein Vergleich zwischen unsigned/unsigned, unsigned/int oder int/int ergeben.
    Natürlich soll das Template alle Möglichkeiten erschlagen können.



  • Was ist jetzt die Frage? Dein Code compiliert jedenfalls. Wunderst du dich vielleicht über den Typ des Rückgabewerts? Da der Rückgabewert zur Compilezeit feststehen muss kann dieser natürlich nicht von den Parametern abhängen. Wenn bei dem Ternären Operator der zweite und dritte Operand einen unterschiedlichen Typ haben hat der gesamte Ausdruck den "Common Type" beider Operanden. Common Type ist der Typ in den man beide Typen implizit konvertieren kann. Das kann manchmal aber auch nicht der Typ sein den man sich wünscht. Wenn ich mir z.B. max((unsigned int)1, (int)-573) ausgeben lasse erhalte ich 4294966723, weil der int Wert zu unsigned konvertiert wird.



  • Ja, das ist das Problem. Die Kombi Int/UInt sollte natürlich im Ergebnis
    als long int rauskommen, weil das der common-type ist, in dem beide Eingaben darstellbar sind. Die implizierte Konvertierung zu unsigned ist natürlich quatsch, die müsste man irgendwie abstellen. Die Definition von common-type passt für diesen Fall offensichtlich nicht, weil int eben nicht einfach zu unsigned konvertiert werden kann auch wenn man es gerne so hinschreibt.
    Die Instantierung des Templates für eine bestimmte Kombination von Eingabetypen soll natürlich auch den folgerichtigen Ausgabetyp beinhalten, was zur Kompilierzeit durchaus prinzipiell festgestellt werden kann.

    Die Frage ist, wie kann ich also allgemeingültig die richtige Konvertierung in den Ergebnistyp vornehmen lassen, ohne mich mit einer Template Speziaisierung für sämliche problematischen Sonderfälle herumschlagen zu müssen.


  • Mod

    MC78 schrieb:

    Ja, das ist das Problem. Die Kombi Int/UInt sollte natürlich im Ergebnis
    als long int rauskommen, weil das der common-type ist, in dem beide Eingaben darstellbar sind.

    Es gibt nur einen common-type, und das ist der, der durch die gewöhnlichen arithmetischen Konvertierungen entsteht (und hier nicht immer der richtige ist).
    Wenn man will, kann man durch Fallunterscheidung immer zu einem vernünftigen Ergebnis kommen. Ob das in Fällen mit gemischten vorzeichenbehaften/vorzeichenlosen Zahlen allerdings sinnvoll ist, darf bezweifelt werden. Weil eben nicht immer unmittelbar erkennbar ist, ob das Ergebnis nun vorzeichenbehaftet ist oder nicht.
    Zu Illustration schnell mal etwas gebastelt:

    #include <limits>
    #include <type_traits>
    #include <algorithm>
    
    using std::min;
    using std::max;
    
    template <typename T>
    constexpr auto digits = std::numeric_limits<T>::digits;
    
    template <typename T, typename U, std::enable_if_t<std::is_unsigned<T>{} && std::is_unsigned<U>{} && !std::is_same<T, U>{}, int> = 0> // unsigned, unsigned
    constexpr auto min(T t, U u)
    {
        using result_t = std::conditional_t<digits<U> < digits<T>, U, T>;                                                                 // kleinerer Typ
        return u < t ? static_cast<result_t>(u) : static_cast<result_t>(t);
    }
    template <typename T, typename U, std::enable_if_t<std::is_signed<T>{} && std::is_signed<U>{} && !std::is_same<T, U>{}, int> = 0>     // signed, signed
    constexpr auto min(T t, U u)
    {
        using result_t = std::conditional_t<digits<T> < digits<U>, U, T>;                                                                 // größerer Typ
        return u < t ? static_cast<result_t>(u) : static_cast<result_t>(t);
    }
    template <typename T, typename U, std::enable_if_t<std::is_unsigned<T>{} && std::is_signed<U>{}, int> = 0>                            // unsigned, signed
    constexpr auto min(T t, U u)
    {
        using result_t = U;
        return u < 0 || static_cast<std::make_unsigned_t<U>>(u) < t ? static_cast<result_t>(u) : static_cast<result_t>(t);
    }
    template <typename T, typename U, std::enable_if_t<std::is_signed<T>{} && std::is_unsigned<U>{}, int> = 0>                            // signed, unsigned
    constexpr auto min(T t, U u)
    {
        using result_t = T;
        return t > 0 && u < static_cast<std::make_unsigned_t<T>>(t) ? static_cast<result_t>(u) : static_cast<result_t>(t);
    }
    
    template <typename T, typename U, std::enable_if_t<std::is_unsigned<T>{} == std::is_unsigned<U>{} && !std::is_same<T, U>{}, int> = 0> // unsigned, unsigned oder signed, signed
    constexpr auto max(T t, U u)
    {
        using result_t = std::conditional_t<digits<U> < digits<T>, U, T>;                                                                 // größerer Typ
        return u < t ? static_cast<result_t>(u) : static_cast<result_t>(t);
    }
    template <typename T, typename U, std::enable_if_t<std::is_unsigned<T>{} && std::is_signed<U>{}, int> = 0>                            // unsigned, signed
    constexpr auto max(T t, U u)
    {
        using result_t = std::conditional_t<digits<T> < digits<std::make_unsigned_t<U>>, std::make_unsigned_t<U>, T>;
        return u >= 0 && t < static_cast<std::make_unsigned_t<U>>(u) ? static_cast<result_t>(u) : static_cast<result_t>(t);
    }
    template <typename T, typename U, std::enable_if_t<std::is_signed<T>{} && std::is_unsigned<U>{}, int> = 0>                            // signed, unsigned
    constexpr auto max(T t, U u)
    {
        using result_t = std::conditional_t<digits<U> < digits<std::make_unsigned_t<T>>, std::make_unsigned_t<T>, U>;
        return t < 0 || static_cast<std::make_unsigned_t<T>>(t) < u ? static_cast<result_t>(u) : static_cast<result_t>(t);
    }
    
    int main()
    {
    	constexpr char c = 1;
    	constexpr unsigned short us = 2;
    	constexpr int i = 3;
    	constexpr long long ll = 4;
    	constexpr unsigned long long ull = 5;
    	static_assert( min( c, us ) == 1, "" );
    	static_assert( std::is_same<decltype(min( c, us )), char>{}, "" );
    	static_assert( max( c, us ) == 2, "" );
    	static_assert( std::is_same<decltype(max( c, us )), unsigned short>{}, "" );
    	static_assert( min( ll, ull ) == 4, "" );
    	static_assert( std::is_same<decltype(min( ll, ull )), long long>{}, "" );
    	static_assert( max( ll, ull ) == 5, "" );
    	static_assert( std::is_same<decltype(max( ll, ull )), unsigned long long>{}, "" );
    }
    

    float-Zahlen müssten man noch separat behandeln, und von gemischten integer/floats lässt man am Besten gleich die Finger.



  • MC78 schrieb:

    Die Instantierung des Templates für eine bestimmte Kombination von Eingabetypen soll natürlich auch den folgerichtigen Ausgabetyp beinhalten, was zur Kompilierzeit durchaus prinzipiell festgestellt werden kann.

    Das ist natürlich machbar. Allerdings würde für dein Problem der Rückgabetyp idealerweise vom Ergebnis des Vergleichs abhängen und das geht meines Wissens nicht (bin aber kein constexpr Experte). Und deine Definition von Common Type funktioniert nicht für alle Typen. Bei Integer möchtest du ja den nächst größeren Typ nehmen aber was wenn man ein uint64_t mit int64_t vergleicht? Einen int128_t gibt es wohl bei den wenigsten Compilern. Oder was ist nach deiner Defintion der Common Type von int und float? Bei int zu float geht eventuell Genauigkeit verloren wenn der int Wert recht groß ist.


  • Mod

    Obige Implementierung nutzt die teilweise sinnvollen UACs nicht aus, und produziert u.U. andere Typen (was AFAICS völlig unnötig ist, da so die Anzahl nachträglicher Konvertierungen des Rückgabewerts insg. nicht vermindert wird). Folgendes ist IMO besser:

    #include <limits>
    #include <type_traits>
    
    template <typename T, typename U>
    using max_t = std::conditional_t<std::numeric_limits<T>::max()    < std::numeric_limits<U>::max()   , U, T>;
    template <typename T, typename U>
    using min_t = std::conditional_t<std::numeric_limits<T>::min()/-2 < std::numeric_limits<U>::min()/-2, U, T>;
    
    template <typename T, typename U>
    constexpr auto min(T t, U u) {
        return (u < 0 && std::is_unsigned<T>{}) || ((t >= 0 || std::is_signed<U>{}) && (min_t<T, U>)u < (min_t<T, U>)t)?
            (min_t<T, U>)u : (min_t<T, U>)t;
    }
    template <typename T, typename U> 
    constexpr max_t<T, U> max(T t, U u) {
        return (t < 0 && std::is_unsigned<U>{}) || ((u >= 0 || std::is_signed<T>{}) && (max_t<T, U>)t < (max_t<T, U>)u)? u : t;
    }
    
    #define CHECK_(f, t, u, R, r) static_assert( f(t,u) == r && std::is_same<decltype(f(t,u)),R>{}, "" );
    #define CHECK(t, u, R1, R2) CHECK_(min, t, u, R1, t) CHECK_(max, t, u, R2, u)
    CHECK((char)-1, (unsigned short)2, char, unsigned short)
    CHECK((short)30000, (int)5641761, int, int)
    CHECK((long long)-651465154, (unsigned)-1, long long, long long)
    CHECK((unsigned)0, (int)1, int, unsigned)
    

    Kompiliert ohne Warnungen (dafür waren die prinzipiell überflüssigen Casts im Vergleich nötig).


  • Mod

    Bei Integer möchtest du ja den nächst größeren Typ nehmen aber was wenn man ein uint64_t mit int64_t vergleicht?

    Hier besteht kein Problem. Wir wissen bei min , dass wir entweder eine negative Zahl als Minimum zurückgeben, oder zwei positive Zahlen vergleichen müssen, in welchem Falle wir den vom vorzeichenbehafteten Typ gespeicherten Wert zum anderen konvertieren. Bei max ist die Argumentation ähnlich. Natürlich setzt das Ganze ggf. zwei Vergleiche voraus, aber der eine ist zum Glück trivial.

    Für gewöhnlich hat man jedoch schon irgendeine Art von Information über die Werte (e.g. der im vorzeichenbehafteten Typ ist positiv), daher sind solche min/max Dinger nur in Spezialfällen nötig.



  • Vielen lieben Dank für die Antworten. Ich glaube ich hab unterschätzt, was die Frage für einen Rattenschwanz hinter sich herzieht.

    Ich glaube, die Antwort von Arcoth halbwegs kapiert zu haben.
    Mit conditional_t wählt man je nachdem, ob der größte darstellbare Wert von T oder U größer ist T oder U als Typ für max_t aus.
    Für die vorzeichenbehafteten Zahlen sollte ja std::numeric_limits<...>::max() kleiner sein, als für die ohne Vorzeichen.
    Mit diesem max_t als Rückgabewert für das max Template umgeht man die Probleme mit der implizierten Konvertierung.
    Was mir nicht so klar ist: Warum muss man das bei dem min Template nicht analog machen? 😉

    Beim vorherigen Post von Camper habe ich mich leider etwas abgehängt gefühlt.

    😕


  • Mod

    MC78 schrieb:

    Beim vorherigen Post von Camper habe ich mich leider etwas abgehängt gefühlt.

    ja, das war unnötig umständlich.

    Arcoth schrieb:

    produziert u.U. andere Typen

    Passiert, wenn man Code ohne gross nachzudenken aus den Fingern saugt 🙂
    Hatte mir irgendwie Fälle zusammengereimt, wo weder T noch U passen, ist aber Unfug.


Log in to reply