[C++] 항목 27: 캐스팅은 절약, 또 절약! 잊지 말자

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역

📦 5. 구현

👉🏻 항목 27: 캐스팅은 절약, 또 절약! 잊지 말자

✅ 캐스팅이란?

C++에서 캐스팅(casting)이란 타입 시스템을 바꾸는 기능을 말한다.
이는 매우 조심히 다뤄야 하는 강력한 기능이다.

캐스팅은 두 가지 스타일로 나뉜다:

  1. 구형 스타일의 캐스트 (C 스타일)

     (T) 표현식
     T(표현식)
    
  2. 신형 스타일의 캐스트 (C++ 스타일)

     const_cast<T>(표현식)
     dynamic_cast<T>(표현식)
     reinterpret_cast<T>(표현식)
     static_cast<T>(표현식)
    

✅ 신형 스타일 캐스트의 종류

각 캐스트는 고유한 목적과 특징을 가진다:

1. const_cast

  • 객체의 상수성을 없애기 위해 사용
  • 상수 객체를 비상수 객체로 캐스팅하는 유일한 방법

2. dynamic_cast

  • 안전한 다운캐스팅(safe downcasting)을 위해 사용
  • 주어진 객체가 어떤 클래스 상속 계통에 속한 특정 타입인지 결정할 때 사용
  • 런타임 비용이 높다 (매우 느림)

3. reinterpret_cast

  • 포인터를 int로 바꾸는 등의 하부 수준 캐스팅을 위해 사용
  • 구현 환경에 의존적이므로 이식성이 없다

4. static_cast

  • 암시적 변환을 강제 수행할 때 사용
  • int → double, 비상수 객체 → 상수 객체
  • 기본 클래스의 포인터 → 파생 클래스의 포인터
  • void* → T*

✅ 신형 스타일을 사용해야 하는 이유

class Widget {
public:
    explicit Widget(int size);
    ...
};

void doSomeWork(const Widget& w);

int main() {
    doSomeWork(Widget(15));                    // 구형 방식
    doSomeWork(static_cast<Widget>(15));       // 신형 방식

    int x, y;
    double d = static_cast<double>(x)/y;       // 신형 방식
}

신형 스타일의 장점:

  1. 코드를 읽을 때 알아보기 쉽다
  2. 캐스트 사용 목적을 더 좁혀 지정하기에, 사용 에러를 진단하기 쉽다

✅ 캐스팅 사용 시 함정들

1. 포인터 주소 문제

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

int main() {
    Derived d;
    Base* pb = &d;  // Derived* -> Base* 암시적 변환
}

문제: Derived와 Base의 포인터 값이 같지 않을 때도 있다.
즉, 객체 하나가 가질 수 있는 주소가 여러 개일 수 있다.

2. 기본 클래스 함수 호출 시 실수

class Window {
public:
    virtual void onResize() { ... }
    ...
};

class SpecialWindow : public Window {
public:
    virtual void onResize() {
        // ❌ 잘못된 방법: 임시 객체가 생성됨
        static_cast<Window>(*this).onResize();
        ...
    }
    ...
};

문제: 캐스팅이 일어나며 *this의 기본 클래스 부분에 대한 사본이 임시 생성된다.
함수 호출이 이루어지는 객체가 현재의 객체가 아니다!

해결:

class SpecialWindow : public Window {
public:
    virtual void onResize() {
        Window::onResize();  // ✅ 올바른 방법
        ...
    }
    ...
};

✅ dynamic_cast의 문제점과 해결책

문제 상황

class Window { ... };
class SpecialWindow : public Window {
public:
    void blink() { ... };
    ...
};

typedef std::vector<shared_ptr<Window>> VPW;

int main() {
    VPW winPtrs;
    ...
    for(VPW::iterator iter = winPtrs.begin();
            iter != winPtrs.end();
            ++iter) {
        if(SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
            psw->blink();
    }
}

문제점:

  • 클래스 이름에 대한 문자열 비교 연산 기반으로 구현되어 있어 정말 느리다
  • 상속 깊이가 깊거나, 다중 상속의 경우 비용이 매우 크다


해결 방법 1: 타입별 컨테이너 사용

typedef std::vector<shared_ptr<SpecialWindow>> VPSW;

int main() {
    VPSW winPtrs;
    ...
    for(VPSW::iterator iter = winPtrs.begin();
            iter != winPtrs.end();
            ++iter) {
        (*iter)->blink();
    }
}

한계: Window에서 파생되는 모든 객체를 담을 수 없다.


해결 방법 2: 가상 함수 활용

class Window {
public:
    virtual void blink() {}  // 아무 동작도 하지 않는다
    ...
};

class SpecialWindow : public Window {
public:
    virtual void blink() { ... }
    ...
};

typedef vector<shared_ptr<Window>> VPW;

int main() {
    VPW winPtrs;
    ...
    for(VPW::iterator iter = winPtrs.begin();
            iter != winPtrs.end();
            ++iter)
        (*iter)->blink();
}

핵심: 원하는 조작을 가상 함수 집합으로 정리하여, 기본 클래스에 넣어두면 된다.


✅ 피해야 할 위험한 설계

폭포식(cascading) dynamic_cast

for(VPW::iterator iter = winPtrs.begin();
        iter != winPtrs.end();
        ++iter) {
    if(SpecialWindow1 * psw1 =
        dynamic_cast<SpecialWindow1*>(iter->get())
    ) { ... }
    else if(SpecialWindow2 * psw2 =
        dynamic_cast<SpecialWindow2*>(iter->get())
    ) { ... }
    else if(SpecialWindow3 * psw3 =
        dynamic_cast<SpecialWindow3*>(iter->get())
    ) { ... }
    ...
}

문제점:

  • 속도가 매우 느리다
  • Window 클래스 계통이 바뀌면, 해당 코드도 함께 수정되어야 한다 → 망가지기 쉽다

🧐 정리

  1. 다른 방법이 가능하다면 캐스팅은 피하자.
    특히 속도가 중요한 코드에서 dynamic_cast는 깊게 고민하자.
  2. 캐스팅이 꼭 필요하다면, 함수 안에 숨길 수 있도록 하자.
    최소한 사용자는 자신의 코드에 캐스팅을 넣지 않고, 해당 함수를 호출할 수 있게 된다.
  3. 구형 스타일의 캐스트보단 C++ 스타일의 캐스트를 선호하자.
    발견하기 쉽고, 어떤 역할을 의도했는지 파악하기 쉽다.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기