4 분 소요

C++ 캐스팅에 대하여

이전 포스팅

암묵적 캐스팅? (C Stype Cast)

A* a = (A*)b;
  • 엄격한 규칙 없이 ‘가능한 캐스팅’을 순차적으로 시도하며
    성공한 것을 선택함
    • 사실상 아래의 4대 캐스팅을 전부 시도하는 방식
  • 사용자의 의도와 상관없이 캐스팅을 가능한 성공시키려고 하기에
    위험한 캐스팅임
    • 코드를 봐도 어떤 종류의 Cast가 적용되었는지 알기 어려움
      • 유지보수가…
    • 별도의 ‘안정성 체크’를 파악하기 힘듦
      • ex) 성공했나? -> 사용 -> reinterpret 캐스팅 기반이여서 UB 발생
      • 임의적으로 const 를 제거한 캐스팅을 사용하기도 함
  • 현대 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<> 참고)
    • VTable을 이용하기에, ‘가상함수’가 없는 경우
      컴파일 에러가 발생
      (허용되지 않은 변환 관련 에러)

    • 업캐스팅 시에는, static_cast와 동일하게 동작함
      (RTTI나 VTable을 사용할 필요 없이, 자식 객체 데이터가 부모 데이터를 포함하므로)
  • 다운 캐스팅 등 실제 객체의 적절한 타입 변환에 실패하는 경우
    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를 붙임)
  • 보통 특정 라이브러리/API 등에서 const를 요구하거나 const로 반환할 때
    const_cast를 사용하는 편

  • 다만 const 를 제거하더라도
    ‘수정’하는 것을 권장하진 않음
    • 컴파일러에 따라 최적화가 되거나 Read-only 영역에 배치도 가능
    • 크래시 or 적용 x 등 UB 발생 가능
    • 정말 수정할 용도라면 const 키워드를 제거하기
  • 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, const_cast, reinterpret_cast
      • static_cast : 캐스팅 대상 타입이 컴파일 시간에 고정
      • const_cast : const와 volatile 키워드를 제거 (타입이 이미 고정)
      • reinterpret_cast : 비트 패턴 재해석
  • 다이나믹 캐스트 참조 실패?
    • 포인터의 경우는 nullptr 반환함
    • 참조의 경우 std::bad_cast 예외(throw)를 발생
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';
}

댓글남기기