HN Debrief

C++ float-to-int conversion can be undefined behavior

  • Programming
  • Compilers
  • Security
  • Developer Tools

The post explains a nasty C++ rule that many developers miss. A float-to-int cast is undefined behavior when the floating-point value cannot be represented by the target integer type. The article uses Microsoft’s Guidelines Support Library, specifically `gsl::narrow`, to show how code meant to make conversions safer can still be built on top of undefined behavior if it performs the cast first and only checks afterward.

If your C++ code casts floats to ints without an explicit range check, treat it as a real bug, not a portability nit. Audit any helper or “safe cast” wrappers too, because compiler optimization can turn this into miscompilation even when the target CPU instruction itself looks well behaved.

Discussion mood

Strongly negative toward the C++ rule and toward any claim that this instance of undefined behavior is harmless. The mood was especially critical because the bug sits inside a safety-branded library API, and because compiler examples showed real miscompilation rather than mere cross-platform numeric differences.

Key insights

  1. 01

    LLVM poison breaks later invariants

    LLVM treats an out-of-range float-to-int conversion as poison, so the bad cast does not just yield a weird integer. It can let the optimizer violate facts that looked guaranteed later in the code. The Godbolt examples made that concrete by producing values that bypassed explicit min logic and by removing a bounds check badly enough to segfault. That changes the bug from “wrong number” to “control-flow corruption inside otherwise ordinary code.”

    Do not rely on post-cast validation to contain this case. Put the range check before the cast, and inspect optimized builds when reviewing numeric conversion helpers.

      Attribution:
    • jcranmer #1
    • mort96 #1
    • afdbcreid #1
  2. 02

    Hardware behavior does not make UB benign

    The pushback against the GSL defense was that CPU conversion instructions are beside the point. Even if every supported processor returns a predictable value for the conversion itself, the compiler is still free to optimize under the assumption that the undefined path never happens. That is why people saw the “benign on our platforms” line as a category error, especially coming from work tied to C++ memory-safety efforts. The later note that GSL reopened the issue reads as an implicit admission that the original rationale does not hold.

    When reviewing low-level code, separate ISA behavior from language semantics. A stable machine instruction does not rescue source code that gives the optimizer license to make impossible-path assumptions.

      Attribution:
    • digitalPhonix #1
    • wavemode #1
    • mort96 #1
    • 20k #1 #2
  3. 03

    This should have been implementation-defined

    Several commenters drew a bright line between portability and correctness. x86 and ARM already produce different results for these conversions, but that alone is not a reason for UB. C++ could have required each implementation to document its chosen result and still kept programs valid. Framing it that way helps explain why people are frustrated. The current rule gives compilers optimization freedom far beyond what hardware differences require.

    If you are designing language rules or internal coding standards, reserve UB for cases where you truly need semantic freedom. Numeric conversions that already map to defined hardware behavior are strong candidates for documented implementation choices instead.

      Attribution:
    • orangepanda #1
    • marcosdumay #1
    • cataphract #1
    • 20k #1
    • gpvos #1
  4. 04

    Inline assembly is one escape hatch

    One practical workaround was to bypass the language cast entirely and call the architecture’s conversion instruction directly with inline assembly, such as `CVTTSS2SI` on x86_64 or `FCVTZS` on AArch64. The point was not portability or elegance. It was that these instructions define outputs for all inputs, so you can get a single-instruction conversion without granting the optimizer UB-based freedom. You still inherit architecture-specific results, but that is a cleaner trade than silent miscompilation.

    Only use this route in hot paths where you have measured a need and can pin behavior per architecture. For general code, a pre-cast range check is still the safer default.

      Attribution:
    • dmitrygr #1

Against the grain

  1. 01

    Supported compiler versions narrow the blast radius

    A few comments argued that if GSL only promises behavior on a tested matrix of specific compiler versions, then relying on what those compilers currently do is not automatically incoherent. That does not turn the code into standard C++, but it does make the risk closer to a vendor compatibility contract than to blind faith. This view reframes the issue as an unsupported portability gamble, not necessarily a guaranteed bug on every declared target today.

    If your project depends on behavior outside the language spec, document the exact compiler and version contract and test it like an ABI. Do not present that as general-purpose safe code, and expect it to break on toolchain upgrades.

      Attribution:
    • Maxatar #1
    • ranger_danger #1 #2
  2. 02

    UB does not literally mean invalid program

    One commenter objected to the common shorthand that any UB makes a program “incorrect by definition.” The standard’s wording is looser than that and explicitly allows implementations to document behavior in UB cases. Others replied that this is technically true but not operationally useful, because once the standard imposes no requirements, developers lose the right to reason about the program as portable C++. The distinction matters if you care about standards language, but it does not make the cast safe.

    Be precise in standards discussions, but do not let terminology soften engineering decisions. Code that can hit UB still needs the same urgency in audits and fixes.

      Attribution:
    • Maxatar #1
    • Joker_vD #1
    • wat10000 #1

In plain english

AArch64
The 64-bit ARM architecture used in many servers, phones, and newer desktop systems.
CVTTSS2SI
An x86 instruction that converts a single-precision floating-point value to a signed integer using truncation.
FCVTZS
An AArch64 instruction that converts a floating-point value to a signed integer using rounding toward zero.
Godbolt
A popular online Compiler Explorer tool used to compile small code examples with different compilers and inspect the generated output or assembly.
GSL
GNU Scientific Library, a numerical computing library for C and C++.
gsl::narrow
A GSL conversion helper intended to perform narrowing casts while checking whether the conversion changed the value.
LLVM
A widely used open-source compiler infrastructure that underpins Clang and many optimization tools.
MSVC
Microsoft Visual C++, Microsoft's C and C++ compiler toolchain.
poison
An LLVM internal value that represents invalid computation and can let later optimizations assume impossible conditions, leading to surprising code transformations.
UB
Undefined behavior, meaning the C++ standard places no requirements on what happens when the program reaches that case.
WG21
The ISO C++ standards committee responsible for evolving the C++ language.
x86_64
The 64-bit version of the x86 processor architecture used in most PCs and many servers.
Xcode
Apple’s development environment, which includes the Apple Clang compiler toolchain.

Reference links

Issue and standards references

Compiler behavior examples

Related discussions and bug reports