reinterpret_cast 변환
reinterpret_cast 변환 (reinterpret_cast conversion)
어떤 타입의 비트 패턴을 그대로 두고 다른 타입으로 다루고 싶을 때, 즉 "원하는 대로 해석"하라는 지시를 컴파일러에게 내리고 싶을 때 쓰는 캐스트가 reinterpret_cast예요. 이 페이지에서 reinterpret_cast가 무엇을 할 수 있고, 어떤 위험이 따르는지(타입 앨리어싱, 호출 비호환)를 설명할게요.
출처: cppreference
본문
reinterpret_cast는 기본 비트 패턴을 다시 해석해서 타입 사이를 변환해요.
구문 (Syntax)
reinterpret_cast<target-type>( expression )
target-type 타입의 값을 반환해요.
설명 (Explanation)
static_cast와 달리, reinterpret_cast는 (정수와 포인터 사이, 또는 포인터 표현이 타입에 의존하는 특이한 아키텍처에서 포인터 사이를 변환할 때를 제외하고) const_cast처럼 어떠한 CPU 명령으로도 컴파일되지 않아요. 그것은 주로 컴파일 타임 지시문으로, expression을 마치 target-type 타입인 것처럼 다루라고 컴파일러에게 지시해요.
다음 변환만 reinterpret_cast로 할 수 있어요. 단 그러한 변환이 constness(또는 volatility)를 캐스트해 없애는 경우는 제외해요.
- 정수·열거형·포인터·멤버 포인터 타입의 표현식을 자신의 타입으로 변환할 수 있어요. 결과 값은
expression의 값과 같아요. - 포인터는 그 타입의 모든 값을 담을 만큼 큰 어떤 정수 타입으로 변환할 수 있어요(예:
std::uintptr_t). - 어떤 정수·열거형 타입의 값도 포인터 타입으로 변환할 수 있어요. 충분한 크기의 정수로 변환했다가 같은 포인터 타입으로 돌아온 포인터는 원래 값을 갖는 것이 보장돼요. 그 외의 결과 포인터는 안전하게 역참조할 수 없어요(반대 방향의 왕복 변환은 보장되지 않아요. 같은 포인터가 여러 정수 표현을 가질 수 있어요). 널 포인터 상수
NULL이나 정수 0은 대상 타입의 널 포인터 값을 산출한다는 보장이 없어요. 그 목적에는static_cast나 암시 변환을 써야 해요. std::nullptr_t타입의 어떤 값(nullptr포함)이라도 마치(void*)0인 것처럼 어떤 정수 타입으로 변환할 수 있어요. 하지만 어떤 값(nullptr조차도)도std::nullptr_t로 변환할 수 없어요. 그 목적에는static_cast를 써야 해요. (since C++11)- 어떤 객체 포인터 타입
T1*도 다른 객체 포인터 타입cv T2*로 변환할 수 있어요. 이것은 정확히static_cast<cv T2*>(static_cast<cv void*>(expression))과 동등해요(T2의 정렬 요구사항이T1보다 더 엄격하지 않으면, 포인터 값은 변하지 않고 결과 포인터를 원래 타입으로 다시 변환하면 원래 값이 나와요). 어쨌든 역참조된 값이 타입 비접근(type-accessible)일 때만 결과 포인터를 안전하게 역참조할 수 있어요. T1타입의 lvalue(until C++11)glvalue(since C++11) 표현식을 다른 타입T2에 대한 참조로 변환할 수 있어요. 결과는*reinterpret_cast<T2*>(p)와 같아요. 여기서p는expression이 지정하는 객체나 함수를 가리키는 "T1에 대한 포인터" 타입의 포인터예요. 임시 객체는 구체화/생성되지(since C++17) 않고, 복사도, 생성자·변환 함수 호출도 없어요. 결과 참조는 타입 비접근일 때만 안전하게 접근할 수 있어요.- 함수에 대한 어떤 포인터든 다른 함수 타입에 대한 포인터로 변환할 수 있어요. 결과는 불특정이지만, 그런 포인터를 원래 함수 타입에 대한 포인터로 다시 변환하면 원래 함수를 가리키는 포인터가 나와요. 결과 포인터는 그 함수 타입이 원래 함수 타입과 호출 비호환(call-compatible)일 때만 안전하게 호출할 수 있어요.
- 어떤 구현에서는(
dlsym이 요구하듯 POSIX 호환 시스템에서 특히) 함수 포인터를void*나 다른 객체 포인터로, 또는 그 반대로 변환할 수 있어요. 구현이 양방향 변환을 지원하면 원래 타입으로의 변환은 원래 값을 산출하고, 그 외의 결과 포인터는 안전하게 역참조하거나 호출할 수 없어요. - 어떤 포인터 타입의 널 포인터 값도 다른 어떤 포인터 타입으로 변환할 수 있고, 그 타입의 널 포인터 값이 돼요. 널 포인터 상수
nullptr이나std::nullptr_t타입의 다른 값은reinterpret_cast로 포인터로 변환할 수 없다는 점을 주의하세요. 그 목적에는 암시 변환이나static_cast를 써야 해요. - 멤버 함수에 대한 포인터를 다른 타입의 다른 멤버 함수에 대한 포인터로 변환할 수 있어요. 원래 타입으로 다시 변환하면 원래 값이 나오고, 그 외의 결과 포인터는 안전하게 쓸 수 없어요.
- 어떤 클래스
T1의 멤버 객체에 대한 포인터를 다른 클래스T2의 다른 멤버 객체에 대한 포인터로 변환할 수 있어요.T2의 정렬이T1보다 더 엄격하지 않으면 원래 타입T1으로 다시 변환하면 원래 값이 나오고, 그 외의 결과 포인터는 안전하게 쓸 수 없어요.
모든 캐스트 표현식처럼 결과는:
target-type이 lvalue 참조 타입이거나 함수 타입에 대한 rvalue 참조(since C++11)면 lvalue,target-type이 객체 타입에 대한 rvalue 참조면 xvalue, (since C++11)- 그 외에는 prvalue.
타입 앨리어싱 (Type aliasing)
타입 접근성 (Type accessibility)
타입 T_ref가 다음 타입 중 하나와 유사(similar)하면, 동적 타입 T_obj의 객체는 T_ref 타입의 lvalue(until C++11)glvalue(since C++11)를 통해 타입 비접근(type-accessible)이 돼요.
char,unsigned char또는std::byte(since C++17): 이것은 어떤 객체의 객체 표현을 바이트 배열로 검사할 수 있게 해줘요.T_objT_obj에 대응하는 signed 또는 unsigned 타입
프로그램이 타입 비접근인 lvalue(until C++11)glvalue(since C++11)를 통해 객체의 저장 값을 읽거나 수정하려 하면, 동작은 undefined예요.
이 규칙은 타입 기반 앨리어스 분석(type-based alias analysis)을 가능하게 해요. 컴파일러는 한 타입의 glvalue로 읽은 값이 다른 타입의 glvalue에 대한 쓰기에 의해 수정되지 않는다고 가정해요(위에 언급한 예외는 제외).
많은 C++ 컴파일러가 비표준 언어 확장으로 이 규칙을 완화해서, 공용체의 비활성 멤버를 통한 잘못된 타입 접근을 허용한다는 점을 주의하세요(C에서는 그런 접근이 undefined가 아니에요).
호출 비호환 (Call compatibility)
다음 조건 중 하나를 만족하면, 타입 T_call은 함수 타입 T_func와 호출 비호환(call-compatible)이 돼요.
T_call이T_func와 같은 타입,T_func*가 함수 포인터 변환을 통해T_call*로 변환될 수 있음. (since C++17)
함수가 그 함수 타입이 호출된 함수의 정의 타입과 호출 비호환인 표현식을 통해 호출되면, 동작은 undefined예요.
참고 (Notes)
정렬 요구사항이 충족된다고 가정하면, 포인터-interconvertible 객체를 다루는 몇 가지 제한된 경우를 제외하고 reinterpret_cast는 포인터 값을 바꾸지 않아요.
struct S1 { int a; } s1;
struct S2 { int a; private: int b; } s2; // not standard-layout
union U { int a; double b; } u = {0};
int arr[2];
int* p1 = reinterpret_cast<int*>(&s1); // value of p1 is "pointer to s1.a" because
// s1.a and s1 are pointer-interconvertible
int* p2 = reinterpret_cast<int*>(&s2); // value of p2 is unchanged by reinterpret_cast
// and is "pointer to s2".
int* p3 = reinterpret_cast<int*>(&u); // value of p3 is "pointer to u.a":
// u.a and u are pointer-interconvertible
double* p4 = reinterpret_cast<double*>(p3); // value of p4 is "pointer to u.b": u.a and
// u.b are pointer-interconvertible because
// both are pointer-interconvertible with u
int* p5 = reinterpret_cast<int*>(&arr); // value of p5 is unchanged by reinterpret_cast
// and is "pointer to arr"
실제로 적절한 타입의 객체를 지정하지 않는 glvalue(reinterpret_cast로 얻은 것 같은)에 대해 비정적 데이터 멤버나 비정적 멤버 함수를 지정하는 클래스 멤버 접근을 수행하면 undefined behavior예요.
struct S { int x; };
struct T { int x; int f(); };
struct S1 : S {}; // standard-layout
struct ST : S, T {}; // not standard-layout
S s = {};
auto p = reinterpret_cast<T*>(&s); // value of p is "pointer to s"
auto i = p->x; // class member access expression is undefined behavior;
// s is not a T object
p->x = 1; // undefined behavior
p->f(); // undefined behavior
S1 s1 = {};
auto p1 = reinterpret_cast<S*>(&s1); // value of p1 is "pointer to the S subobject of s1"
auto i = p1->x; // OK
p1->x = 1; // OK
ST st = {};
auto p2 = reinterpret_cast<S*>(&st); // value of p2 is "pointer to st"
auto i = p2->x; // undefined behavior
p2->x = 1; // undefined behavior
많은 컴파일러가 이런 경우 "strict aliasing" 경고를 내요. 기술적으로 이런 구조는 흔히 "strict aliasing rule"로 알려진 문단이 아닌 다른 것에 어긋나지만요.
strict aliasing과 관련 규칙의 목적은 타입 기반 앨리어스 분석을 가능하게 하는 거예요. 관련 없는 타입에 대한 두 포인터(예: int*와 float*)가 동시에 존재해 둘 다 같은 메모리를 로드·저장하는 데 쓸 수 있는 상황을 프로그램이 유효하게 만들 수 있다면, 그 분석은 무너져요. 따라서 그런 상황을 만들 수 있어 보이는 어떤 기법도 반드시 undefined behavior를 불러일으켜요.
객체의 바이트를 다른 타입의 값으로 해석해야 한다면 std::memcpy나 std::bit_cast(since C++20)를 쓸 수 있어요.
double d = 0.1;
std::int64_t n;
static_assert(sizeof n == sizeof d);
// n = *reinterpret_cast<std::int64_t*>(&d); // Undefined behavior
std::memcpy(&n, &d, sizeof d); // OK
n = std::bit_cast<std::int64_t>(d); // also OK
구현이 std::intptr_t와/또는 std::uintptr_t를 제공하면, 객체 타입이나 cv void에 대한 포인터에서 이 타입들로의 캐스트는 항상 잘 정의돼요. 그러나 함수 포인터에는 보장되지 않아요. (since C++11)
C에서는 집합체 복사·할당이 집합체 객체 전체를 접근해요. 하지만 C++에서 그런 동작은 항상 멤버 함수 호출을 통해 수행되며, 객체 전체가 아니라 개별 하위 객체를 접근해요(공용체의 경우 객체 표현, 즉 unsigned char로 복사해요).
키워드 (Keywords)
reinterpret_cast
예제 (Example)
reinterpret_cast의 몇 가지 사용을 보여줘요.
#include <cassert>
#include <cstdint>
#include <iostream>
int f() { return 42; }
int main()
{
int i = 7;
// pointer to integer and back
std::uintptr_t v1 = reinterpret_cast<std::uintptr_t>(&i); // static_cast is an error
std::cout << "The value of &i is " << std::showbase << std::hex << v1 << '\n';
int* p1 = reinterpret_cast<int*>(v1);
assert(p1 == &i);
// pointer to function to another and back
void(*fp1)() = reinterpret_cast<void(*)()>(f);
// fp1(); undefined behavior
int(*fp2)() = reinterpret_cast<int(*)()>(fp1);
std::cout << std::dec << fp2() << '\n'; // safe
// type aliasing through pointer
char* p2 = reinterpret_cast<char*>(&i);
std::cout << (p2[0] == '\x7' ? "This system is little-endian\n"
: "This system is big-endian\n");
// type aliasing through reference
reinterpret_cast<unsigned int&>(i) = 42;
std::cout << i << '\n';
[[maybe_unused]] const int &const_iref = i;
// int &iref = reinterpret_cast<int&>(
// const_iref); // compiler error - can't get rid of const
// Must use const_cast instead: int &iref = const_cast<int&>(const_iref);
}
가능한 출력:
The value of &i is 0x7fff352c3580
42
This system is little-endian
42
결함 보고서 (Defect reports)
- CWG 195 (C++98): 함수 포인터와 객체 포인터 사이의 변환이 허용되지 않았음 → 조건부 지원으로.
- CWG 658 (C++98): 포인터 변환의 결과가 불특정이었음(원래 타입으로의 변환 제외) → 가리키는 타입이 정렬 요구사항을 만족하는 포인터에 대한 지정 제공.
- CWG 799 (C++98):
reinterpret_cast로 무엇을 할 수 있는지가 불명확했음 → 명확히 함. - CWG 1268 (C++11):
reinterpret_cast가 lvalue를 참조 타입으로만 캐스트할 수 있었음 → xvalue도 허용. - CWG 2780 (C++98):
reinterpret_cast가 함수 lvalue를 다른 참조 타입으로 캐스트할 수 없었음 → 허용. - CWG 2939 (C++17):
reinterpret_cast가 prvalue를 rvalue 참조 타입으로 캐스트할 수 있었음 → 금지.
참고 (References)
- C++23 표준(ISO/IEC 14882:2024): 7.6.1.10 Reinterpret cast [expr.reinterpret.cast]
- C++20 표준(ISO/IEC 14882:2020): 7.6.1.9 Reinterpret cast [expr.reinterpret.cast]
- C++17 표준(ISO/IEC 14882:2017): 8.2.10 Reinterpret cast [expr.reinterpret.cast]
- C++14 표준(ISO/IEC 14882:2014): 5.2.10 Reinterpret cast [expr.reinterpret.cast]
- C++11 표준(ISO/IEC 14882:2011): 5.2.10 Reinterpret cast [expr.reinterpret.cast]
- C++98 표준(ISO/IEC 14882:1998): 5.2.10 Reinterpret cast [expr.reinterpret.cast]
- C++03 표준(ISO/IEC 14882:2003): 5.2.10 Reinterpret cast [expr.reinterpret.cast]
더 알아보기
- const_cast 변환 — const/volatile 추가·제거
- static_cast 변환 — 기본 변환 수행
- dynamic_cast 변환 — 검사된 다형적 변환 수행
- 명시적 캐스트 (explicit casts) — 타입 사이의 관대한 변환
- 표준 변환 (standard conversions) — 한 타입에서 다른 타입으로의 암시 변환
- bit_cast (C++20) — 한 타입의 객체 표현을 다른 타입의 것으로 다시 해석(함수 템플릿)