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.
-
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.
-
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).
-
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. Beimax
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.
-
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.