Programming/C, C++

[C++] 타입 캐스팅 (static_cast, dynamic_cast) 대하여

변화의 물결1 2025. 5. 10. 00:05

 

안녕하세요,

 

 

1. C++ 타입 캐스팅 필요성

 

 C++은 타입 시스템을 가지고 있어, 서로 다른 타입 간의 데이터 교환이나 연산을 엄격하게 제한합니다. 하지만 때로는 프로그래머의 의도에 따라 타입을 변환해야 하는 경우가 발생합니다.

 

 예를 들어, 부모 클래스 포인터가 실제로는 자식 클래스 객체를 가리키고 있을 때, 자식 클래스 고유의 멤버에 접근하려면 타입 변환이 필요합니다. 이럴 때 사용하는 것이 바로 타입 캐스팅 연산자입니다.

 

C++ 에는 크게 네 가지의 명시적 타입 캐스팅 연산자가 있습니다.

 (1) static_cast<T>(표현식)

 (2) dynamic_cast<T>(표현식)

 (3) const_cast<T>(표현식)

 (4) reinterpret_cast<T>(표현식)

 

각각의 캐스팅 연산자는 사용 목적과 특징이 다르므로, 상황에 맞게 올바르게 사용하는 것이 중요합니다.

 

 

2. 캐스팅 연산자 종류 간략 소개

 

  static_cast와 dynamic_cast를 알아보기 전에, 네 가지 캐스팅 연산자에 대해 간략히 살펴보겠습니다.

 

 (1) static_cast : 컴파일 시점에 타입 변환의 유효성을 검사합니다. 주로 서로 관련된 타입 간의 변환(예: 상속 관계에서의 업캐스팅/다운캐스팅, 기본 자료형 간의 변환 등)에 사용됩니다. 관련 없는 타입 간의 변환을 시도하면 컴파일 에러가 발생합니다.

 

 (2) dynamic_cast : 런타임(실행 시간)에 타입 변환의 안전성을 검사합니다. 주로 다형성을 가진 클래스(하나 이상의 가상 함수를 가진 클래스)의 포인터나 참조에 대해 다운캐스팅 또는 크로스캐스팅을 안전하게 수행할 때 사용됩니다.

 변환이 안전하지 않다고 판단되면 포인터의 경우 nullptr을, 참조의 경우 std::bad_cast 예외를 발생시킵니다.

 

 (3) const_cast : 객체의 const 또는 volatile 한정자를 제거하거나 추가할 때 사용합니다. const로 선언된 객체의 상수성을 제거하는 것은 매우 주의해서 사용해야 하며, 원래 const가 아니었던 객체에 대해 const를 제거하는 용도로 사용됩니다.

 

(4) reinterpret_cast : 서로 관련 없는 포인터 타입 간의 변환, 포인터와 정수 간의 변환 등에 사용되며, 비트 단위의 재해석을 수행합니다.  매우 위험하며, 필요한 경우가 아니라면 사용을 피해야 합니다.

 

 

3. static_cast

 

1) 정의과 특징

 

 컴파일 시간에 타입 변환을 수행하며, 비교적 명확하고 예측 가능한 변환에 합니다.  static_cast 특징으로는 타입 변환의 논리적 오류(예: 전혀 관련 없는 타입 간의 포인터 변환)를 컴파일러가 어느 정도 감지할 수 있습니다. 그리고 실행 시간에 추가적인 검사를 수행하지 않으므로 성능 저하가 없습니다.

 

 Upcasting (업캐스팅) 시에는 대부분 안전하며, 암시적으로도 변환될 수 있지만 명시적으로 static_cast를 사용할 수도 있습니다.

 

 Downcasting (다운캐스팅) 시에는 static_cast를 사용한 다운캐스팅은 컴파일러가 안전성을 보장해주지 않습니다. 즉, 프로그래머가 해당 변환이 안전하다는 것을 확신할 때 사용해야 합니다.

 

2) static_cast 예제 및 분석

 

#include <iostream>

class BasePerson {
public:
    virtual ~BasePerson() {}
    void speak() { std::cout << "Person speaks.\n"; }
};

class BaseCar {
public:
    virtual ~BaseCar() {}
    void drive() { std::cout << "Car drives.\n"; }
};

int main() {
    //<Case 1> static_cast
    BasePerson* person = new BasePerson;
    BaseCar* car = new BaseCar;

    // C 스타일 캐스팅 (매우 위험하며 권장하지 않음)
    // BasePerson* p_from_car_c_style = (BasePerson*)car;
    // p_from_car_c_style->speak(); // BaseCar 객체에 speak()가 없으므로 런타임 오류 가능성 높음

    // static_cast를 사용한 관련 없는 타입 간의 변환 시도로 컴파일 시점에 에러를 발생시킵니다.
    // BasePerson과 BaseCar는 상속 관계가 없는 전혀 다른 타입이기 때문입니다.
    // person = static_cast<BasePerson*>(car); // 컴파일 에러!

    if (person) delete person;
    if (car) delete car;

    return 0;
}

 

 

 C 스타일 캐스팅 (BasePerson*)car는 컴파일러가 특별한 경고 없이 통과시킬 수 있지만, 이는 car 포인터가 가리키는 메모리를 BasePerson 타입으로 강제 해석하겠다는 의미이므로 위험한 코드가 됩니다.

 

 만약 이렇게 변환된 포인터로 BasePerson의 멤버 함수를 호출하려 한다면, 예기치 않은 동작이나 프로그램 충돌이 발생할 수 있습니다.

 

 

4. dynamic_cast

 

1) 정의와 특징

 

 실행 시간에 타입 변환의 안전성을 검사하며, 주로 다형적 클래스 계층 구조에서 포인터나 참조를 안전하게 다운캐스팅하거나 크로스캐스팅할 때 사용합니다.

 

특징으로는 실행 시간에 실제 객체의 타입을 확인하여 변환이 안전한 경우에만 성공적으로 변환합니다.

 dynamic_cast를 사용하려면 변환의 대상이 되는 클래스(또는 그 부모 클래스)가 최소 하나 이상의 가상 함수(일반적으로 가상 소멸자)를 가져야 합니다. 이는 RTTI(Run-Time Type Information)를 사용하기 위함입니다.

 

다운캐스팅 시에는 부모 타입 포인터/참조를 자식 타입 포인터/참조로 변환할 때, 실제 객체가 해당 자식 타입이거나 그 자식 타입의 파생 클래스인 경우에만 성공하며, 실패 시 포인터는 nullptr을 반환하고, 참조는 std::bad_cast 예외를 발생시킵니다.

 

2) dynamic_cast를 사용한 다운캐스팅 예제 및 분석

 

예제 클래스 상속 관계는 다음과 같습니다.

 

 

 

#include <iostream>

class BaseType {
public:
    virtual ~BaseType() {}
    virtual void basePrint() { std::cout << "BaseType print\n"; }
};

class DerivedType1 : public BaseType {
public:
    void print() {
        std::cout << "Type1::print()\n";
    }
    void basePrint() override { std::cout << "DerivedType1's version of basePrint\n"; }
};

class DerivedType2 : public BaseType {
public:
    void print() {
        std::cout << "Type2::print()\n";
    }
    void basePrint() override { std::cout << "DerivedType2's version of basePrint\n"; }
};

int main() {

    BaseType* a = new BaseType();
    DerivedType1* b = static_cast<DerivedType1*>(a);
    std::cout << "--- Case 2: static_cast (b) ---\n";

    if (b != nullptr) {
        // b->print(); //DerivedType1 객체를 가리키지 않으므로 호출은 위험
        b->basePrint();
    }

    DerivedType1* c = dynamic_cast<DerivedType1*>(a);
    std::cout << "--- Case 2: dynamic_cast (c) ---\n";

    if (c != nullptr) {
        std::cout << "dynamic_cast c print()\n";
        c->print(); // 이 코드는 실행되지 않음
    }

    delete a;

    BaseType* a_real_derived1 = new DerivedType1();
    std::cout << "--- Case 2: dynamic_cast with actual DerivedType1 object ---\n";
    DerivedType1* c_success = dynamic_cast<DerivedType1*>(a_real_derived1);

    if (c_success != nullptr) {
        c_success->print(); // 성공적으로 DerivedType1의 print() 호출
    }

    delete a_real_derived1;
    return 0;
}

  

 

 

<소스 코드 설명 (Case 2)>

 

DerivedType1* b = static_cast<DerivedType1*>(a);

static_cast는 컴파일 시점에 a가 BaseType*이고 DerivedType1이 BaseType을 상속받았으므로 다운캐스팅 자체는 허용합니다.

 

 b->print(); (원본 코드에서는 주석 처리되지 않았지만, 호출 시점에는 b가 DerivedType1 객체를 가리킨다는 보장이 없음)를 호출하면, BaseType 객체에는 DerivedType1의 print() 멤버 함수가 없으므로 정의되지 않은 동작을 할 수 있습니다. 

 

DerivedType1* c = dynamic_cast<DerivedType1*>(a);

a는 BaseType 객체를 가리키고 있고, DerivedType1 타입이 아닙니다. 따라서 다운캐스팅은 안전하지 않습니다.

그러므로 dynamic_cast는 nullptr을 반환하고, c는 nullptr이 됩니다.

 

if (c != nullptr) 조건문은 false가 되어, c->print()는 호출되지 않습니다.

 

 

관계 다이어그램으로 나타내면 아래와 같습니다.

 

 

 

3) dynamic_cast를 사용한 크로스캐스팅 예제 및 분석

 

 크로스캐스팅은 같은 부모 클래스를 상속받는 형제 클래스 간의 타입 변환을 의미합니다.

 dynamic_cast는 이러한 변환을 안전하게 시도할 수 있습니다.

 

// 윗 부분은 동일하며, main 부분만 변경되었다고 했을 경우
int main() {
    std::cout << "--- Case 3: dynamic_cast ---\n";
    DerivedType1* d_orig = new DerivedType1();
    DerivedType2* e_orig = dynamic_cast<DerivedType2*>(d_orig);

    if (e_orig != nullptr) {
        std::cout << "dynamic_cast e_orig print()\n"; // 이 코드는 실행되지 않음
        e_orig->print();
    }

    delete d_orig;
    return 0;
}

 

 

<소스 코드 설명>

 

DerivedType2* e_orig = dynamic_cast<DerivedType2*>(d_orig);

d_orig가 가리키는 실제 객체는 DerivedType1이지, DerivedType2가 아닙니다. 따라서 이 크로스캐스팅은 안전하지 않습니다. 결과적으로 dynamic_cast는 nullptr을 반환하고, e_orig는 nullptr이 됩니다.

 

 if (e_orig != nullptr) 조건문은 false가 되므로, e_orig->print()는 호출되지 않습니다.

  

 관계 다이어그램으로 나타내면 아래와 같습니다.

 

 

  

 C 스타일 캐스팅은 코드만 봐서는 어떤 종류의 변환이 일어나는지 명확히 알기 어려워 가독성을 떨어뜨리고 의도를 파악하기 어렵게 만듭니다. 그렇다고 C 스타일 캐스팅이 무조건 나쁘다고 볼 수는 없습니다. 그 만큼 자유도가 있다는 것이고 그에 따른 책임이 발생한는 것입니다.

 

 의도를 명확히 하고 안전성을 높이기 위해 C++에서는 고유의 캐스트 연산자를 사용하는 것을 추천합니다.

 

 

감사합니다.

 

 

<참고사이트>

1. static_cast conversion

https://en.cppreference.com/w/cpp/language/static_cast

2. Mermaid Live Editor

https://mermaid.live/   

 

 

반응형