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



  • @SeppJ

    Was genau stört dich?

    Folgende Sachen finde ich etwas merkwürdig

    • Die Unterstützung von Gleitkommazahlen und insbesonders std::numeric_limits<Target>::digits < std::numeric_limits<Source>::digits) sowie die entsprechende Argumentation der KI.
    • Einen Cast erlauben zu wollen, wenn der cast Source -> Target -> Source das gleiche Ergebnis liefert. Ich vermute dass das Ganze nicht eindeutig ist. Ich schätze dass es z.B. zwei uint64_t Werte gibt, welche nach float gecastet den gleichen Wert in Binärdarstellung gibt.

    Ich habe hierzu Copilot benutzt. Und in der letzten Zeit häufen sich meiner Meinung nach die schweren Fehler.


  • Mod

    Ich dachte, du meintest mit der Zeile 16 wäre speziell was falsch, weil du die hervorgehoben hattest. Die ist in Ordnung. Der ganze Rest ist gequirlter Unsinn, ja. Auf so vielen Ebenen Quatsch, dass ich dazu gar keine Kritik schreiben kann. Die Signatur ergibt ja noch nicht einmal Sinn; gescheitert im 0. Schritt.



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

    Auf so vielen Ebenen Quatsch,

    Ok danke, das bestätigt meine Vermutungen.


  • Mod

    Bezüglich der Grundidee, mir dem round trip: Das ist halt eine philosophische Frage, was du möchtest. Wenn A zu B wird und B zu A wird, ist dann nicht A äquivalent zu B? Das kann ich durchaus als mögliche Definition einer Gleichheit sehen



  • Ich habe das von Hand überarbeitet, und die ifs rausgeworfen, da diese als requires clause sinnvoller sind. Die ::digits Statements sind irgend wie sinnfrei, ich habe das so interpretiert, dass es keinen Wertverluste geben soll. Entspricht das nun eher Deinen Vorstellungen? Der Code aus der AI war eher Schrott, da er z.B. der Wertverlust bei der Konversion gar nicht abfängt.

    #include <cstdlib>
    #include <cassert>
    #include <cstdint>
    #include <type_traits>
    #include <limits>
    
    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 bool numeric_cast_valid(const Target T, const Source Value) {
            const decltype(T) CastedValue = static_cast<Target>(Value);
            const Source ValueRecasted    = static_cast<Source>(CastedValue);
    
            if (Value == ValueRecasted) return true;
    
            return false;
    }
    
    
    int main() {
        float   f = 0;
        int16_t v = 200;
    
        // Ziel
        // int8_t c1 = numeric_cast<int8_t>(v);   // -> Fehler
        // uint8_t c2 = numeric_cast<uint8_t>(v); // -> OK
    
        assert(numeric_cast_valid(f, v));
    
        return EXIT_SUCCESS;
    }
    
    


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


Anmelden zum Antworten