Search

C++ std::bit_cast와 reinterpret_cast — 언제 어떤 것을 써야 하는가

URL
생성 일시
2026/03/23 11:06
최종 편집 일시
2026/03/23 11:06
태그
D2
파일과 미디어
저희 팀은 네이버의 고성능 검색 엔진 기술을 계승한 분산 데이터베이스인 Dot을 개발하고 있습니다. 스토리지 계층을 직접 다루다 보니 디스크에서 읽어 온 바이트 패턴을 uint64_t, float, 혹은 사용자 정의 구조체로 해석하는 일이 일상이고, 코드 리뷰에서 reinterpret_cast를 마주치는 일도 잦습니다. 볼 때마다 느끼는 불안함, 즉 '이것은 되도록 쓰지 말라고 배웠는데 괜찮을까?' 하는 감정은 C++ 개발자라면 공감하실 것입니다. C++20에서 등장한 std::bit_cast는 그 불안에 대한 해답처럼 보였습니다. 표준 라이브러리가 제공하는 함수라는 점, 위험해 보이는 타입 퍼닝(type punning)까지 척척 해낸다는 점이 '이것은 안전하다'라는 착각을 심어 주었습니다. 그 착각이 퍼지면서 팀은 std::bit_cast가 주는 안도감에 젖어 들었고, 예전 같으면 reinterpret_cast를 썼을 자리를 std::bit_cast가 차지하기 시작했습니다. 그러다 어느 날, 다음과 같은 코드를 보았습니다. const uint8_t* src = ...; float* dst = std::bit_cast<float*>(src); // 컴파일 통과, 경고 없음 const를 아무런 경고 없이 제거해 버리는 코드였습니다. 뭔가 잘못되었다는 것은 직감했지만, '정확히 왜 잘못되었는가'를 설명하지 못했습니다. std::bit_cast와 reinterpret_cast의 정확한 의미론(semantics)을 몰랐기 때문에, 잘못되었다는 말을 꺼내기가 어려웠습니다. 이 글은 그 경험에서 시작되었습니다. 저희는 std::bit_cast를 제대로 이해하지 못한 채 쓰고 있었고, reinterpret_cast는 막연히 두려워하고만 있었습니다. 둘 다 제대로 알아야 했습니다. 바이트 패턴을 C++ 타입으로 해석할 때, std::bit_cast와 reinterpret_cast 중 무엇을 써야 할까요? 이 질문에 명확히 답하기 위해, 두 캐스트의 의미론과 그 배경에 있는 엄격한 앨리어싱 규칙(strict aliasing rule), 포인터와 정수 간 변환을 정리합니다. 이 내용만 알아도 reinterpret_cast를 두려움 없이 올바르게 쓸 수 있습니다. reinterpret_cast에는 객체 수명(object lifetime)이라는 더 깊은 규칙이 있습니다. 하지만 실무에서는 몰라도 문제가 되지 않으므로, 해당 내용은 다음 글(예정)에서 다룹니다. 더 깊은 내용이 궁금하신 분만 읽어 보시기를 권합니다. 타입 퍼닝: std::bit_cast는 어떤 문제를 풀기 위해 만들어졌는가 타입 퍼닝은 어떤 값의 비트 패턴을 다른 타입으로 재해석하는 작업입니다. 예를 들어 uint32_t 값 0x41480000의 비트 패턴을 IEEE 754 float로 해석하면 12.5f가 됩니다. 비트 조작, 직렬화, 해시 함수 등에서 흔하게 쓰이는 작업입니다. 이처럼 간단해 보이는 작업을 C++에서 올바르게 수행하는 방법은 의외로 까다롭습니다. 여러 시도를 살펴보겠습니다. 시도 1: reinterpret_cast uint32_t i = 0x41480000; float f = *reinterpret_cast<float*>(&i); // UB! 가장 직관적인 방법이지만 미정의 동작(UB, undefined behavior)입니다. uint32_t 객체를 float*로 접근하는 것은 엄격한 앨리어싱 규칙 위반입니다. 컴파일러는 float*와 uint32_t*가 같은 메모리를 가리키지 않는다고 가정할 수 있으므로, 최적화 과정에서 예상과 다른 결과가 나올 수 있습니다(엄격한 앨리어싱 규칙의 자세한 내용은 엄격한 앨리어싱 규칙에서 다룹니다). 시도 2: union union { uint32_t i; float f; } u; u.i = 0x41480000; // i가 active member가 됨 float f = u.f; // f는 비활성 멤