Round-Trip-Cast als Prüfung für verlustfreien Cast?



  • So empfinde ich den Cast irgend wie sinnvoller, ohne assert weil die Randbedinungen schon bei der Übersetzung gescheckt werden, und ein Compilerfehler geworfen wird.

    #include <cstdlib>
    #include <cassert>
    #include <cstdint>
    #include <type_traits>
    #include <limits>
    #include <iostream>
    
    template <typename Target, typename Source>
    requires(
      std::is_integral_v<Source> &&
      std::is_floating_point_v<Target> &&
     (std::numeric_limits<Target>::digits10 >= std::numeric_limits<Source>::digits10)
    )
    constexpr Target numeric_cast(const Source Value) {
            return static_cast<Target>(Value);
    }
    
    
    int main() {
        int16_t v = 200;
    
        float f = numeric_cast<float>(v);
    
        std::cout << f << std::endl;
    
        return EXIT_SUCCESS;
    }
    
    

  • Mod

    Ich würde digits und so komplett ignorieren. Die Eigenschaften des Typs haben ja überhaupt keine Bedeutung für Wertverluste. Und sind sowieso eher grobe Anhaltspunkte, statt konkreter Merkmale. Die Zahl 100 aus einem Typ mit unbegrenzter Genauigkeit kann problemlos in ein einziges Byte gestopft werden und zurück. Die Zahl 0.1 aus einem Dezimaltyp mit nur einer Nachkommastelle kann hingegen niemals exakt in einen binären Fließkommatyp umgewandelt werden, egal wie exakt der ist.



  • @SeppJ sagte in Round-Trip-Cast als Prüfung für verlustfreien Cast?:

    Ich würde digits und so komplett ignorieren. Die Eigenschaften des Typs haben ja überhaupt keine Bedeutung für Wertverluste.

    Natürlich kann es zu Wertverlusten kommen, wenn der Typ zu klein ist. Das Problem ist, dass Du ohne große Verrenkungen nicht in der Lage bist den Wertebereich zum Übersetzungszeitpunkt zu checken.

    emplate <typename Target, typename Source>
    requires(
      std::is_integral_v<Source> &&
      std::is_floating_point_v<Target>
    )
    constexpr Target numeric_cast(const Source Value) {
            constexpr __int128_t one = 1;
            constexpr __int128_t digits = std::numeric_limits<Target>::digits;
            constexpr __int128_t max_size = one << digits;
    
            if (Value > max_size) throw std::runtime_error("Value does not fit into Target");
    
            return static_cast<Target>(Value);
    }
    

    numeric_cast enthält dann eine if-Abfrage, die man leider nicht constexpr machen kann, da es bisher keine constexpr Parameter für Funktionen gibt. Man kann natürlich eine Template mit einem Zahlenparameter bauen, der dann in der Lage ist, den Wertebereich zu prüfen.

    P.S. Die 128Bit des Integer Typens ist notwendig, weil long double bei vielen Compiler als Quad Precision interpretiert wird.

    Die Zahl 0.1 aus einem Dezimaltyp mit nur einer Nachkommastelle kann hingegen niemals exakt in einen binären Fließkommatyp umgewandelt werden, egal wie exakt der ist.

    Da hier die Funktion immer von Integer zu Floating Point konvertiert, gibt es das Problem nicht.



  • @john-0
    @SeppJ

    Es ist so angedacht dass ich meinen cast immer dann einsetze, wenn ich denke: "Ich muss eine (Integer) Variable in eine andere (Integer) Variable casten und weis dass der Wert immer in das Ziel passt". Dies möchte ich aber mit meinem cast überprüfen. Wenn ich also eine -1 nach uint8_t wandele, so muss ein Fehler geworfen werden.

    Der folgende Code muss dadurch anschlagen, obwohl der Round-Trip Cast funktioniert:

    #include <cassert>
    #include <cstdint>
    
    int main() {
        int8_t c1 = -1;
        uint8_t c2;
        int8_t c3;
    
        c2 = static_cast<uint8_t>(c1);  // c2 = 255 -> Hier wirft mein cast einen Fehler.
        c3 = static_cast<int8_t>(c2);
        assert(c1 == c3);
        return 0;
    }
    

    Mein Cast ist hauptsächlich für Integer gedacht und die KI hat diese auch für Gleitkommazahlen erweitert. Und diese Erweiterung habe ich erst einmal in eine Testreihe gepackt und dabei fiehlen diverse Dinge auf.

    Und die Gleitkomma-Unterstützung und der Round-Trip Cast haben mich irgentwie verwirrt. Insbesonders da mein erster Test std::numeric_limits<unsigned int>::max() -> float unter 32 Bit funktionierte und unter 64 Bit anschlug.



  • @SeppJ sagte in Round-Trip-Cast als Prüfung für verlustfreien Cast?:

    Die Zahl 0.1 aus einem Dezimaltyp mit nur einer Nachkommastelle kann hingegen niemals exakt in einen binären Fließkommatyp umgewandelt werden, egal wie exakt der ist.

    Deswegen habe ich auch den Teil Integer -> Gleitkomma entfernt.



  • @Quiche-Lorraine sagte in Round-Trip-Cast als Prüfung für verlustfreien Cast?:

    Und die Gleitkomma-Unterstützung und der Round-Trip Cast haben mich irgentwie verwirrt. Insbesonders da mein erster Test std::numeric_limits<unsigned int>::max() -> float unter 32 Bit funktionierte und unter 64 Bit anschlug.

    Das hängt von der verwendeten Plattform ab. Wenn Du zuerst auf ILP32 testet, funktioniert die Konversion, da die Mantisse des FP64 Formats groß genug ist. Auf einer LP64 Plattform funktioniert das auch, aber es gibt unter Windows ILP64 Compiler bzw. Compilermodi, und da geht das nicht, da die 64 Bit des Integer Formats nicht in die 53 Bit der Mantisse passen.


  • Mod

    @Quiche-Lorraine sagte in Round-Trip-Cast als Prüfung für verlustfreien Cast?:

    Mein Cast ist hauptsächlich für Integer gedacht und die KI hat diese auch für Gleitkommazahlen erweitert.

    Okay, Integer kriegen wir hin, ganz ohne Philosophie über Bedeutung von Gleichheit 🙂 .Da ist alles ganz klar definiert, und es gibt nur eine Handvoll Fälle (Signed/Unsigned; mehr/weniger Bits). Da kann man ganz klar prüfen, ob es theoretische Grenzen gibt, und ob man über diesen liegt. Aber wichtig: Man muss aufpassen, dass man bei den Vergleichen keine falschen Integerkonvertierungen macht! Denn wenn man schon auf dem Integertypen mit dem höchsten Rang, aber unterschiedlichem Vorzeichen ist, passieren sonst ungewollte Wertumwandlungen.

    Dann haben wir:

    • Gleiche Signage; Zieltyp größer/gleich: Automatisch sicher (Theoretisch könnte der Zieltyp noch anders gewichtet um die 0 herum liegen, aber wir nehmen mal an, dass wir nicht auf einem außerirdischen Computer laufen 👽 )
    • Gleiche Signage, Zieltyp ist kleiner: Wert muss innerhalb der Grenzen von numeric_limits::min/max des Zieltyps liegen. Der Vergleich wird durchgeführt mit dem größeren Quelltyp und man hat keine Probleme mit Konvertierung.
    • Signed nach Unsigned, Zieltyp größer/gleich: Wert muss >= 0 sein (dem min des Zieltyps). Das ist auch gleichbedeutend damit, dass er zwischen den Grenzen der numeric_limits des Ziels liegt, aber wenn wir unnötigerweise auf max prüfen, machen wir ggf. Konvertierungsfehler beim Vergleich. Daher lassen wir das lieber sein.
    • Signed nach Unsigned, Zieltyp kleiner: Wert muss innerhalb der Grenzen von numeric_limits::min/max des Zieltyps liegen. Der Vergleich wird durchgeführt mit dem größeren Quelltyp und man hat keine Probleme mit Konvertierung.
    • Unsigned nach Signed, Zieltyp ist größer: Automatisch sicher, außer auf außerirdischen Maschinen
    • Unsigned nach Signed, Zieltyp ist kleiner/gleich: Wert muss unterhalb max des Zieltyps liegen; siehe oben zu Konvertierungsfehlern der min-Vergleiche, die wir vermeiden, indem wir diese unnötige Bedingung gar nicht prüfen. Der Vergleich passiert im größer/gleichen Quelltyp (oder einer noch größeren Promotion), der in jedem Fall das max des Zieltyps korrekt aufnehmen kann.

    Die Größe würde ich dabei tatsächlich mit sizeof vergleichen. Dann braucht man nicht nachdenken, ob die Vergleiche zwischen den Limits der größten Integertypen funktionieren wie man denkt.

    Die aufgezählten Branches sind statische Eigenschaften der Typen und können mit Templates spezialisiert werden. Aber meine Kaffeepause ist vorbei, das programmiere ich jetzt nicht aus. Sollte relativ einfach runter zu schreiben sein.



  • @SeppJ sagte in Round-Trip-Cast als Prüfung für verlustfreien Cast?:

    Man muss aufpassen, dass man bei den Vergleichen keine falschen Integerkonvertierungen macht! Denn wenn man schon auf dem Integertypen mit dem höchsten Rang, aber unterschiedlichem Vorzeichen ist, passieren sonst ungewollte Wertumwandlungen.

    Ich habe hierzu einfach std::numeric_limits und die std::cmp_equal, cmp_not_equal, cmp_less, cmp_greater, cmp_less_equal, cmp_greater_equal Funktionen benutzt, welche ich durch clang-tidy kenne,


  • Mod

    @Quiche-Lorraine sagte in Round-Trip-Cast als Prüfung für verlustfreien Cast?:

    @SeppJ sagte in Round-Trip-Cast als Prüfung für verlustfreien Cast?:

    Man muss aufpassen, dass man bei den Vergleichen keine falschen Integerkonvertierungen macht! Denn wenn man schon auf dem Integertypen mit dem höchsten Rang, aber unterschiedlichem Vorzeichen ist, passieren sonst ungewollte Wertumwandlungen.

    Ich habe hierzu einfach std::numeric_limits und die std::cmp_equal, cmp_not_equal, cmp_less, cmp_greater, cmp_less_equal, cmp_greater_equal Funktionen benutzt, welche ich durch clang-tidy kenne,

    Uhh, die kannte ich noch gar nicht. Dann kannst du all die Problemfälle vergessen, die ich genannt habe. Die Beispielimplementierung ist ja quasi identisch zu meiner Liste. Man muss es nur noch richtig zusammensetzen. Und es funktioniert sogar auf außerirdischen Computern, wenn die einen standardkonformen C++-Compiler haben.

    PS: std::in_range ist dann die Komplettlösung deines Problems?



  • Ich hab hier jetzt nicht alles detailliert durchgelesen aber irgendwie erinnert mich das an das Paper von Stroustrup. Ganz konkret an Kapitel 2. Vll hilft es ja 🙂
    Concept-Based Generic Programming in C+


Anmelden zum Antworten