4 분 소요

C++ Casting Type

최근 C++에 대하여 다시 공부하며, TIL을 작성하려 한다
그 중, 이전에 공부하였지만 TIL을 작성하지 않은 것들을 위주로 몇몇 내용을 갱신해보려 한다

C++의 명시적 캐스팅에는 4가지 형 변환이 존재하며,
그에 따른 ‘캐스팅’ 연산자를 제공한다

static_cast

컴파일 시간에 타입을 체크
여러 기본 자료형 간의 타입 변화나 (ex : int -> float)
포인터 변화(보통 ‘상속’ 관계에 해당하는 클래스 구조에 사용)

static_cast는 ‘컴파일’ 시점에 ‘타입 변환의 유효성’을 검사한다
물론 실제 변환된 값은 ‘런타임’에서 사용이 되기에
‘동적 할당’을 사용한 개체라도, static_cast를 통하여 타입 변환을 할 수 있다

class Base {};
class Derived : public Base {};

Base* basePtr = new Derived; // 동적 할당
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 유효한 변환

그러나, 아래와 같은 경우 적절한 변환이 이루어지지 않는 점에 유의할 것

Base* basePtr = new Base; // 실제로 Base 객체를 가리킴
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 위험한 변환

실제 객체가 Derived 타입이 아니기에,
만약 상속받은 클래스의 함수를 호출하는 것은 ‘정의되지 않은 결과’를
일으킬 수 있다는 점을 알아야 한다
(이 경우, C 스타일 캐스팅과 유사한 결과가 된다)

그래도 ‘코드 작성자’의 ‘의도’, ‘타입 안정성’ 측면에서
static_cast를 활용하는 것이 훨씬 좋은 편

dynamic_cast

런타임에 타입을 체크
다형성을 가진 부모 클래스와 자식 클래스 사이의 포인터, 참조 변환에 사용
‘안전한 다운 캐스팅’을 제공한다
(해당 변환이 불가능한 경우, 포인터는 nullptr 반환, 참조는 bad_cast 예외 발생)

class Base { virtual void dummy() {} };
class Derived: public Base { int a; };

Base* b = new Derived;
Derived* d = dynamic_cast<Derived*>(b); // 런타임에 안전하게 다운캐스팅

다만 dynamic_cast를 사용하려면 ‘클래스’에 하나 이상의 가상 함수가 있어야 하며,
이 과정에서 RTTI(Run-Time Type Information)를 사용하여 객체의 실제 타입을 확인한다

(‘가상 함수’가 필요하다는 부분에서, ‘가상 함수 테이블’과 연관되어 있다고 추측하여 조사)

* dynamic_cast가 실제 객체 타입을 확인하는 방식
 '가상 함수' : virtual 키워드를 붙인 함수로, 자식 클래스에서 재정의(오버라이드)할 수 있다 (다형성)

 '가상 함수 테이블' : 컴파일러가 생성하는 '가상 함수 주소'를 가진 테이블
 (이를 통해 런타임에서 어떤 함수를 호출할지 결정한다)

 dynamic_cast는 실제 타입을 확인하기 위하여 '가상 함수 테이블'을 이용한다
 해당 객체의 함수 주소를 통하여 '객체'를 변환하려는 '클래스'나 '그 자식의 클래스'인지 확인한다
 (변환이 불가능한 경우에 대한 처리도 이 같이 한다)

이러한 ‘확인 과정’이 필요하기에,
dynamic_cast는 성능 오버헤드가 발생한다
(그렇기에, 이전 게임 회사에서 대부분의 경우에 dynamic_cast를 활용하지 않았으며,
‘static_cast’를 ‘타입 변수’에 따라 사용하는 방식으로 응용하여 사용하였다)
(ex : ItemType이 IT_WEAPON1 인 경우, Weapon 으로 static_cast 하는 방식)

const_cast

const_cast는 특정 변수나 개체의 const(상수) 나 volatile 속성을 추가하거나 제거할 때 사용
const를 제거하여 변수를 수정하는데 주로 사용하지만,
const 로 선언한 것을 수정하는 것이기에 주의가 필요하다
(애초에 ‘수정하는 것을 원하지 않아’ const로 선언하였기에)
(같은 타입에서 const만 제거하기에 다른 캐스팅과는 다소 다른 편)

* volatile 키워드
컴파일러 최적화를 방지하는 키워드로,
해당 키워드를 붙인 경우, 프로그램은 항상 해당 변수의 값을 '메모리'에서 읽어오게 된다
(캐시나 레지스터에 값을 저장하지 않도록 한다)

'항상' 최신의 값을 읽어오도록 함으로서 '변수가 예상치 못하게 변경되는 것'을 어느정도는 방지할 수 있다
(그렇기에 멀티 스레드 환경에서 '공유 변수'로 활용될 수 있음)
그러나 'atomic'(원자성)을 보장할 수는 없기에, 보통은 동기화 도구를 사용하는 것이 더 권장된다

보통은 특정 라이브러리나 API가 const를 요구하거나 반환할 때,
해당 요구 사항을 맞추기 위하여 사용하는 것이 주 용도가 된다

const int a = 10;

int& b = const_cast<int&>(a);
b = 20; // const_cast를 사용하여 a의 const를 제거 (b 변경 시, a값 변경)

추가적으로,
원래부터 const로 ‘선언’된 ‘객체’의 경우는
const_cast를 적용하는 것이 ‘정의되지 않은 행동’을 유발할 수 있다고 한다

-> 컴파일러의 최적화 방식과 const 객체의 형태, 실행 환경 등에 따라서
const 개체가 ‘최적화’되며 해당 객체의 데이터가 ‘Code’ 영역 등에 나뉘어 저장이 될
가능성이 있기에 이러한 상태의 const 개체를 ‘const_cast’로 건드리게 되면
‘Read Only’영역에 접근하여 kill 될 수 있다
(그러나 이것이 항상 그렇다고 볼 수 없기에 ‘정의되지 않은 행동’)

reinterpret_cast

reinterpret_cast는 ‘타입 비트’를 유지하며 ‘타입’만을 변화하는 캐스팅 방식이다
(즉, 메모리는 냅두고 메모리의 ‘해석’만 바뀐다는 관점)

// char 포인터를 int 포인터로 변환하는 예제입니다.
char c = 'a';
char* cp = &c;
int* ip = reinterpret_cast<int*>(cp);

위의 예시에서는 ‘a’를 가리키는 char 포인터를
int 포인터로 캐스팅한 것으로 *ip의 값은
‘a’를 int로 바꾼 값이 된다(ASCII)

특히 바이너리 데이터를 다룰 때, 유용한 캐스팅이다

struct MyData {
    int a;
    float b;
};

char buffer[sizeof(MyData)]; // 바이너리 데이터를 저장할 버퍼

MyData data = {10, 3.14f}; // 전송하고자 하는 데이터

// MyData 구조체를 바이너리 데이터로 변환
std::memcpy(buffer, &data, sizeof(MyData));

// 나중에 buffer에서 MyData 구조체로 다시 변환
MyData* ptr = reinterpret_cast<MyData*>(buffer);

서버로 전송하는 Data나, 특정 ‘파일’에 저장하는 경우
그 값을 불러올 때, 목적인 클래스로 캐스팅 하는 방식으로 사용할 수 있다
(추가적으로 포인터를 ‘아주 큰 정수형’으로 변환할 수 있도록 해준다)
(포인터 주소값을 정수형 변수로 담는 방식)

그러나 ‘타입 안전성’을 보장하지 않기에,
‘실제로’ 타입 변환이 되는지를 정확히 알아야 한다
-> ‘안전’하지 않은 캐스팅으로 유명하며, 보통 특수한 상황이 아니라면 사용을 자제하는 편이 좋다
(위의 바이너리 데이터 사용 방식도, ‘직렬화’에 관련된 라이브러리의 사용이 안전하고 유지보수에 좋을 수 있음)

C Style Casting

C++에서 이러한 ‘명시적’ 연산을 제공하는 이유는
C 스타일 캐스팅의 ‘모호함’ 때문인데

#include <iostream>

class Base {
public:
    virtual void print() { std::cout << "Base" << std::endl; }
};

class Derived : public Base {
public:
    void print() override { std::cout << "Derived" << std::endl; }
    void derivedOnly() { std::cout << "Derived only method" << std::endl; }
};

int main() {
    Base* b = new Derived();

    // C 스타일 캐스팅을 사용하여 Derived 클래스의 메소드 호출
    ((Derived*)b)->derivedOnly();

    // C 스타일 캐스팅을 사용하여 정수를 포인터로 변환
    int i = 42;
    Base* strange = (Base*)(i);

    // 컴파일은 되지만, 실행 시 정의되지 않은 동작을 유발할 수 있음
    strange->print();

    delete b;
    return 0;
}

이러한 코드가 있을 때,

  1. derivedOnly를 호출하는 경우, dynamic_cast를 호출하는 것이 런타임 시 안전(dynamic_cast는 변환 불가 시 nullptr 반환)
  2. int 타입의 ‘i’를 Base 포인터로 캐스팅(reinterpret_cast와 유사한 방식의 캐스팅)
    -> 해당 부분이 작성자가 ‘의도’하였는지를 파악하기 힘들며
    해당 코드는 ‘정의되지 않은 동작’이 발생

C 스타일 캐스팅은
‘타입 안정성’을 보장하지 않을 수 있으며,
코드의 의도를 명확하게 전달하지 못할 수 있는 면이 존재한다

C 스타일 캐스팅은 ‘C’ 코드와의 호환을 위해 남겨진 기능에 가깝다
(그 외에는 그냥 혼자 코드 짤때는 편하다던가)

태그:

카테고리:

업데이트:

댓글남기기