# C++의 부동소수점→정수 변환이 정의되지 않은 동작을 일으키는 경우

> Clean Markdown view of GeekNews topic #32002. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=32002](https://news.hada.io/topic?id=32002)
- GeekNews Markdown: [https://news.hada.io/topic/32002.md](https://news.hada.io/topic/32002.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-07-31T09:01:44+09:00
- Updated: 2026-07-31T09:01:44+09:00
- Original source: [kttnr.net](https://kttnr.net/blog/cpp-float-to-int-conversion-undefined-behavior/)
- Points: 2
- Comments: 1

## Topic Body

- `float`의 소수부를 버린 값이 대상 정수형 범위를 벗어나면 **정의되지 않은 동작(UB)** 이 발생하며, 암시적 변환·함수형 캐스트·`static_cast` 모두 영향받음
- `-Wall`과 `-Wextra`는 이를 경고하지 않고, `-Wconversion`도 **암시적 변환만 탐지**해 놓치기 쉬움
- Microsoft GSL의 안전한 축소 변환 함수 `gsl::narrow`도 일부 부동소수점→정수 입력에서 UB를 일으켜, 표현할 수 없는 값에 예외를 던진다는 문서의 동작을 지키지 못함
- x86의 `CVTTSS2SI`는 표현 불가능한 값을 `INT_MIN`으로 처리하지만, AArch64의 `FCVTZS`는 포화 변환하고 NaN을 0으로 바꾸므로 **하드웨어별 결과가 달라질 수 있음**
- 안전하게 변환하려면 **캐스트 전에 범위를 검사**해야 하며, Clang·GCC의 UBSan 옵션 `-fsanitize=float-cast-overflow`로 문제를 탐지할 수 있음

---

### 변환 규칙과 탐지의 한계
- [C++ 부동소수점-정수 변환 규칙](https://en.cppreference.com/w/cpp/language/implicit_conversion)에 따르면, 소수부를 버린 뒤 값이 대상 정수형에 들어가지 않으면 **정의되지 않은 동작**이 됨
  - 대상이 unsigned여도 모듈러 산술은 적용되지 않음
  - `int i0 = f`, `int(f)`, `static_cast&lt;int&gt;(f)` 모두 일부 입력에서 UB를 일으킴
- 일반적인 컴파일러 경고만으로는 문제를 모두 찾기 어려움
  - `-Wall`과 `-Wextra`는 세 변환 모두 경고하지 않음
  - `-Wconversion`은 암시적 변환만 경고함
- 현재 프로세서와 컴파일러에서 프로그램이 계속 실행되더라도 결과는 플랫폼마다 달라질 수 있음
  - x86의 `CVTTSS2SI`는 표현할 수 없는 입력을 `INT_MIN`으로 매핑함
  - AArch64의 `FCVTZS`는 포화 처리하고 NaN을 0으로 매핑함
  - 실행된 UB는 컴파일러가 다른 변환을 적용할 때 코드가 갑자기 오작동하는 원인이 될 수 있음

### GSL 사례와 안전한 대응
- Microsoft [Guidelines Support Library](https://github.com/microsoft/GSL)의 [`gsl::narrow`](https://github.com/microsoft/GSL/blob/a75212b9f3b14162edd62d540cbf9273d5a59d20/docs/headers.md#gslnarrow)는 대상 형식으로 표현할 수 없는 값에 예외를 던지는 안전한 축소 변환을 표방함
  - 실제 부동소수점→정수 변환은 일부 입력에서 먼저 **UB를 실행**하므로 문서와 일치하지 않음
  - GSL 측은 대상 플랫폼에서 하드웨어 트랩 표현을 건드리지 않아 내부 UB가 무해하다고 판단했으며, 이 논리가 [코드](https://github.com/microsoft/GSL/blob/a75212b9f3b14162edd62d540cbf9273d5a59d20/include/gsl/narrow#L47)에 반영된 채 문제는 수정되지 않음
- 올바른 해결책은 **캐스트 전에 범위를 검사**하는 것임
  - Rust의 포화 변환 방식을 바탕으로 한 [cpp-clamp-cast 개념 증명 라이브러리](https://github.com/e00E/cpp-clamp-cast/)가 있음
- Clang과 GCC의 [Undefined Behavior Sanitizer](https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html)에서 `-fsanitize=float-cast-overflow`를 사용하면 해당 UB를 탐지할 수 있음
  - 모든 C++ 코드를 UBSan으로 테스트하는 방식을 권장함

## Comments



### Comment 62651

- Author: neo
- Created: 2026-07-31T09:02:44+09:00
- Points: 1

###### [Lobste.rs 의견들](https://lobste.rs/s/ba2yfy/c_float_int_conversion_can_be_undefined) 
- C와 C++의 미묘한 미정의 동작을 많이 알고 있었는데도 이 사례는 놀라웠음  
  Rust도 LLVM IR의 같은 부동소수점→정수 변환 규칙을 물려받아 한동안 미정의 동작이 있었고, 2020년에야 더 복잡한 IR을 생성하도록 수정함. 성능 차이가 커서 값이 유한하고 대상 타입 범위에 들어간다는 걸 아는 고성능 반복문용으로 [검사 없는 부동소수점→정수 변환](https://doc.rust-lang.org/nightly/std/primitive.f64.html#method.to_int_unchecked)도 제공함  
  C++ Core Guidelines Library가 이를 대수롭지 않게 여기는 건 터무니없음. LLVM은 변환에서 얻은 정보를 이용해 경계 검사를 제거했고, 그 결과 [경계 검사가 있었는데도 배열 범위를 벗어난 사례](https://github.com/rust-lang/rust/issues/10184#issuecomment-139858153)가 있음. **메모리 안전성**과 미정의 동작 회피를 진지하게 여긴다면 이를 무시할 수 없으며, Herb Sutter가 “양성 미정의 동작”을 말한 것도 실망스러움

- C++에 미정의 동작이 많다는 건 알았지만 이것은 특히 놀라움. 변환을 최대한 빠르게 만들려 했고, 아키텍처마다 이 극단값을 다르게 처리하는 명령어가 있어서 **미정의 동작**으로 둔 것인지 궁금함. 부호 있는 정수 오버플로와 비슷하게 이런 규칙이 생기는 주된 이유로 보임
  - 그 이유라면 미정의 동작이 아니라 **구현 정의 동작**이어야 함. 0으로 나누기는 일부 아키텍처에서 트랩되므로 미정의 동작인 것이 이해됨  
    미정의 동작은 해제 후 사용처럼 C 추상 기계 밖의 효과 때문에 같은 플랫폼에서도 일관된 결과를 보장할 수 없거나, 일부 대상에서 트랩될 수 있는 경우로 제한해야 함  
    표준 준수 구현은 미정의 동작을 자체적으로 정의할 수 있고 GCC도 일부 항목에서 그렇게 함. 안정적인 의미를 성능 비용 없이 보장할 수 있다면 대상별 부동소수점→정수 명령어의 동작을 그대로 정의해도 되지만, 다른 구현에서는 여전히 미정의 동작이 됨
  - 아마 그럴 것임. PowerPC의 `fctiw` 계열은 포화 변환하며 NaN을 `INT_MIN`으로 바꾸고 FPSCR 플래그도 설정함. 64비트 Power ISA의 `fctid`도 더 큰 정수에 같은 처리를 하지만, 어느 쪽도 AArch64나 x86의 동작과 일치하지 않음

- 이런 경우에는 **구현 정의 동작**이나 지정되지 않은 값으로 처리하는 편이 맞음. 무한대를 `int`로 변환했다는 이유만으로 프로그램 전체가 C++ 표준의 통제를 벗어나는 건 말이 안 됨  
  [C++26은 몇 가지 불합리한 미정의 동작을 제거했으며](https://www.sandordargo.com/blog/2026/07/29/cpp26-reduces-undefined-behaviour), 이것도 명백한 제거 후보임. 실험상 GCC와 Clang이 이 미정의 동작을 최적화에 활용하지 않는 듯해 실제 영향은 제한적으로 보임
  - 반례가 있음: https://godbolt.org/z/bqnoMMrz8
  - 링크된 이슈의 [댓글](https://github.com/microsoft/GSL/issues/786#issuecomment-5127325234)에 따르면 Clang은 실제로 이 규칙을 **최적화에 활용함**

- 명세가 이 연산을 **미정의 동작**으로 규정할 이유가 없는 또 하나의 불쾌한 사례임

- IEEE 754를 싫어할 이유가 하나 더 생김. 다른 라이브러리나 성능 때문에 꼭 필요하지 않다면 부동소수점 대신 순수 정수, 큰 정수 분자·분모를 쓰는 유리수, 고정소수점 십진수를 최대한 사용함
  - 이번 사태는 **IEEE 754와 무관**하며 전적으로 C++의 잘못임

- C나 C++를 사용했다면 `float32`와 `int32`, 심지어 `int64`의 차이도 놀라운 일이 아니어야 함. 큰 지수를 쓰는 `float32`는 `int64`보다 훨씬 큰 정숫값을 나타낼 수 있음  
  서로의 상위 집합이 아니고 표현 능력이 다른 형식 사이에서, 언어 문법과 무관하게 **부동소수점→정수 변환**이 안전하다고 가정할 이유가 없음
  - 핵심을 놓치고 있음. 부동소수점의 모든 값을 보존할 수 없다는 것과 **미정의 동작**은 다름  
    `uint32_t`도 `uint64_t`의 모든 값을 표현할 수 없지만 변환 의미는 잘라내기로 정의되어 있음. 여기서는 특정 입력에서 컴파일러가 무엇이든 할 수 있다는 게 본질적으로 다른 문제임
  - 표현 범위가 다르다고 안전한 변환을 정의할 수 없는 것은 아님. Rust는 [부동소수점→정수 변환을 명시적으로 정의](https://doc.rust-lang.org/reference/expressions/operator-expr.html#r-expr.as.numeric.float-as-int)하고, [Java 26 언어 명세](https://docs.oracle.com/javase/specs/jls/se26/jls26.pdf)는 5.1.3절에서 변환 절차를 상세히 규정함  
    저수준 언어라면 `CVTTSS2SI`나 `FCVTZS` 같은 어셈블리 명령어에 대응시키는 방식도 가능함. 저수준 언어가 어셈블리와 밀접하고 다른 “양성” 미정의 동작도 논란이 큰 만큼, 이 변환이 미정의 동작을 허용한다는 사실이 오히려 더 놀라움
  - 하드웨어에 따라 구현별 쓰레기 값을 내거나, 하드웨어·검사 도구가 트랩해 프로그램을 중단하는 것은 크게 놀랍지 않음  
    하지만 무관한 코드를 이상하게 오컴파일하거나, 하드 드라이브를 포맷하거나, [“코에서 악마가 날아 나오게”](https://en.wikipedia.org/wiki/Undefined_behavior) 하는 것까지 허용된다는 건 놀라움  
    이중 해제나 배열 범위 밖 쓰기에는 무엇이든 일어날 수 있다는 미정의 동작 개념이 타당하지만, C와 C++는 구현 정의 쓰레기 값이나 프로그램 중단처럼 더 엄격한 규칙을 둘 수 있는 곳에도 이를 남발함. Rust는 일부 정수 오버플로를 디버그 모드에서 트랩하고 릴리스 모드에서 지정되지 않은 값을 반환하지만, 엉뚱한 코드까지 망가뜨리지는 못하게 함  
    C++가 Java나 Rust가 될 필요는 없지만, 불필요한 **미정의 동작을 줄이면** 분명 더 나은 언어가 됨
  - 나에게는 새로운 사실이었음. 표현할 수 없는 부동소수점을 정수로 변환하면 결과 정수에 엉터리 값이 들어가는 정도로 생각했지, 프로그램 전체가 영원히 오염되어 **C++ 표준의 통제 밖**으로 나간다고는 몰랐음
