C++ Cast
C++ 캐스팅에 대하여
암묵적 캐스팅? (C Stype Cast)
A* a = (A*)b;
- 엄격한 규칙 없이 ‘가능한 캐스팅’을 순차적으로 시도하며
성공한 것을 선택함
- 사실상 아래의 4대 캐스팅을 전부 시도하는 방식
- 사실상 아래의 4대 캐스팅을 전부 시도하는 방식
- 사용자의 의도와 상관없이 캐스팅을 가능한 성공시키려고 하기에
위험한 캐스팅임
- 코드를 봐도 어떤 종류의 Cast가 적용되었는지 알기 어려움
- 유지보수가…
- 유지보수가…
- 별도의 ‘안정성 체크’를 파악하기 힘듦
- ex) 성공했나? -> 사용 -> reinterpret 캐스팅 기반이여서 UB 발생
- 임의적으로 const 를 제거한 캐스팅을 사용하기도 함
- ex) 성공했나? -> 사용 -> reinterpret 캐스팅 기반이여서 UB 발생
- 코드를 봐도 어떤 종류의 Cast가 적용되었는지 알기 어려움
- 현대 C++ 에선 추천하지 않음!
C++의 4대 캐스팅
- static_cast
- dynamic_cast
- const_cast
- reinterpret_cast
static_cast
- 컴파일 시간에 타입 체크
- 실제 변환된 값을 ‘런타임’에서 사용할 수 있기에
동적 할당된 객체도 static_cast를 통해 타입 변환 가능
- 실제 변환된 값을 ‘런타임’에서 사용할 수 있기에
- 업캐스팅, 기본 타입 변환 등에 안전하게 사용할 수 있는 변환
- 업캐스팅의 경우, 자식 객체 내부에 ‘부모 클래스 데이터’가 존재함
- 업캐스팅의 경우, 자식 객체 내부에 ‘부모 클래스 데이터’가 존재함
- 서로 상속 관계 없는 클래스 변환 시도 컴파일 오류 발생
class Parent {};
class Child : public Parent {};
Child c;
Parent* p = static_cast<Parent*>(&c); // 안전
class A {};
class B {};
A a;
A* ap = &a;
B* bp = static_cast<B*>(ap); // ❌ 컴파일 오류
- 다만 다운 캐스팅을 통해 static_cast로 시도하는 경우는
위험할 수 있음
- 상속 관계는 있으나, 실제 부모 개체를 자식 포인터로 캐스팅하여도 에러를 내뱉지 않음
(이 경우, 정의되지 않은 동작 가능) - 다운 캐스팅의 런타임 체크가 반드시 필요한 경우라면 dynamic_cast를 고려
- 상속 관계는 있으나, 실제 부모 개체를 자식 포인터로 캐스팅하여도 에러를 내뱉지 않음
class Parent { public: int a; };
class Child : public Parent { public: int b; };
Parent p; // 실제 객체는 Parent
Parent* pp = &p;
Child* cp = static_cast<Child*>(pp); // 컴파일 O
cp->b = 10; // ❌ UB (메모리 쓰레기 덮어쓰기)
dynamic_cast
- 런타임에 타입을 체크
- RTTI(Run-Time Type Information) 설정을 통해
생성된 type_info를 통해 확인
- 반대로 RTTI를 ‘끄는 경우’ dynamic_cast가 동작하지 않음
(여담으로 Unreal은 RTTI 옵션을 끈다 -> 아래의 Cast<> 참고)
- 반대로 RTTI를 ‘끄는 경우’ dynamic_cast가 동작하지 않음
-
VTable을 이용하기에, ‘가상함수’가 없는 경우
컴파일 에러가 발생
(허용되지 않은 변환 관련 에러) - 업캐스팅 시에는, static_cast와 동일하게 동작함
(RTTI나 VTable을 사용할 필요 없이, 자식 객체 데이터가 부모 데이터를 포함하므로)
- RTTI(Run-Time Type Information) 설정을 통해
-
다운 캐스팅 등 실제 객체의 적절한 타입 변환에 실패하는 경우
nullptr 반환을 하기에 확실히 타입을 체크할 수 있음 - 다만 여러 검사로 인하여 static_cast에 비하여 무거운 편
- 너무 남발하지 않는다면 그래도 용납 가능한 수준
(ex : Tick 등)
- 너무 남발하지 않는다면 그래도 용납 가능한 수준
class Base { public: virtual ~Base(){} };
class Derived : public Base {};
// 안전한 다운 캐스팅
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // OK
// 실체가 부모인데 자식으로 다운 캐스팅 함
Base* b = new Base();
Derived* d = dynamic_cast<Derived*>(b); // d == nullptr
const_cast
- 특정 타입의 const / volatile 속성 제거/ 추가용 캐스팅
- const를 제거하여 변수를 수정 가능
- const를 추가하는 방식도 가능
- 다만 const를 사용하는 이유 자체가 ‘수정 불가능’을 노린 것이기에
주의가 필요함
(애초에 같은 프로젝트 내부에서 ‘수정’이 필요한 경우였다면
그냥 const 키워드를 제거하는 편이 더 합리적인 판단)
(반대로 임의의 개체가 수정되지 않기를 원하면 처음부터 const를 붙임)
- const를 제거하여 변수를 수정 가능
-
보통 특정 라이브러리/API 등에서 const를 요구하거나 const로 반환할 때
const_cast를 사용하는 편 - 다만 const 를 제거하더라도
‘수정’하는 것을 권장하진 않음
- 컴파일러에 따라 최적화가 되거나 Read-only 영역에 배치도 가능
- 크래시 or 적용 x 등 UB 발생 가능
- 정말 수정할 용도라면 const 키워드를 제거하기
- 컴파일러에 따라 최적화가 되거나 Read-only 영역에 배치도 가능
- volatile 키워드?
- 컴파일러의 최적화를 막는 용도(레지스터에 값 보관 금지)
- CPU 캐시를 사용하지 말고, 실제 메모리 주소에서 읽도록 함
- 멀티 스레드 환경에서 바뀐 값 등을 확인하고 쓰고 싶은 경우 등에 사용 가능
(다만, 동기화 보장은 없기에 atomic, Mutex 같은 동기화 도구와 같이 사용할 것)
- 컴파일러의 최적화를 막는 용도(레지스터에 값 보관 금지)
const int x = 10; // const 객체
int* p = const_cast<int*>(&x);
*p = 20; // ❌ UB
// 제거하고 다른 함수 호출
void foo(char* s);
void foo(const char* s);
const char* msg = "hello";
foo(const_cast<char*>(msg)); // 특정 오버로드 호출
reinterpert_cast
- 메모리 비트 단위로 ‘지정 타입’으로 읽어버리는 캐스팅
(메모리 비트는 내버려 두고, 메모리의 ‘해석’을 변환하는 방식)
- 비트 패턴 변경
- 비트 패턴 변경
- 바이너리 데이터를 다룰 때는 유용하기에
로우 레벨의 데이터 저장 / 서버로 보내는 데이터 등
‘해석’할 수 있는 방법이 있는 경우 사용 가능
long num = 0x11223344;
char* bytes = reinterpret_cast<char*>(&num);
- 그러나 타입 안정성을 전혀 보장하지 않고
UB 발생 확률도 높아서 일반적으로는 권장하지 않는 캐스팅
- 유지보수 면으로도 좋지 않음
- 유지보수 면으로도 좋지 않음
- 위에서 말한 데이터 사용 방식 등도
기본적으로는 관련 라이브러리 사용을 권장하는 편
- 안정성이 검증된 라이브러리 함수를 사용하는 것이
유지보수에 좋음
- 안정성이 검증된 라이브러리 함수를 사용하는 것이
TMI - Unreal의 Cast<>
C++의 캐스팅은 전반적인 타입과 RTTI(Dynamic_Cast)로 동작
Unreal의 Cast<>는 UObject 계열만 대상으로하는 Dynamic_cast에 가까움
-
Unreal의 Reflection 시스템 기반
(IsA, StaticClass, UClass 등) -
잘못된 캐스팅 시 nullptr 반환
(RTTI 대신 위의 리플렉션이 기반) -
UObject 및 언리얼 Interface 등에 사용 가능
(CastCheck, CastFailed 등 변형 함수들도 존재)
// 대략적 내부 구조
template <typename T>
T* Cast(UObject* Source)
{
if (Source && Source->IsA(T::StaticClass()))
{
return static_cast<T*>(Source);
}
return nullptr;
}
- RTTI 를 의존하지 않는 Unreal 특화 캐스팅
- 타입 안정성을 보장
- 런타임 비용이 아주 없지는 않기에 Tick 등에서 호출하는 것을 권장하진 않음!
- 타입 안정성을 보장
TMI - 세부 질문 관련
- 업캐스팅에 활용할 수 있는 캐스팅?
-
- static_cast, dynamic_cast
- 업캐스팅은 자식 객체에서 부모 객체 데이터를 알기에
static_cast로도 파악 가능
- static_cast, dynamic_cast
-
- 컴파일 시간에 변환 타입이 확정되는 캐스팅들은?
- static_cast, const_cast, reinterpret_cast
- static_cast : 캐스팅 대상 타입이 컴파일 시간에 고정
- const_cast : const와 volatile 키워드를 제거 (타입이 이미 고정)
- reinterpret_cast : 비트 패턴 재해석
- static_cast : 캐스팅 대상 타입이 컴파일 시간에 고정
- static_cast, const_cast, reinterpret_cast
- 다이나믹 캐스트 참조 실패?
- 포인터의 경우는 nullptr 반환함
- 참조의 경우
std::bad_cast예외(throw)를 발생
- 포인터의 경우는 nullptr 반환함
struct Base { virtual ~Base() {} };
struct Derived : Base {};
Base* b = new Base();
Derived* d1 = dynamic_cast<Derived*>(b); // nullptr
try {
Derived& d2 = dynamic_cast<Derived&>(*b); // bad_cast 예외
} catch(const std::bad_cast& e) {
std::cout << e.what() << '\n';
}
댓글남기기