스토리텔링 개발자

[Effective C++] 27. 캐스팅 본문

개발/Effective C++

[Effective C++] 27. 캐스팅

김디트 2024. 6. 21. 11:16
728x90

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

 

 

 

C++의 동작 규칙
  • 어떤 일이 있어도 타입 에러가 생기지 않도록 보장한다.
  • 즉, 이론적으로는 컴파일만 깔끔하게 끝나면 그 이후엔 어떤 객체에 대해서도 불안전한 연산이나 말도 안되는 연산을 수행하려 들지 않는다.
  • 허나 공교롭게도 이 타입 시스템을 우회하는 방법이 있으니, 그것이 캐스트(cast)이다.

 

 

 

C++에서 지원하는 캐스팅 문법
  • C 스타일 캐스트 (구형 스타일 캐스트)
    1. (T)n
      • n을 T 타입으로 캐스팅
    2. T(n)
      • n을 T 타입으로 캐스팅
  • C++ 스타일 캐스트 (신형 스타일 캐스트)
    1. const_cast(n)
      • 객체의 상수성을 없애는 용도로 사용된다.
      • 상수성을 없애는 기능을 가진 캐스트는 이것 뿐이다.
    2. dynamic_cast(n)
      • 안전한 다운 캐스팅을 할 때 사용하는 연산자.
      • 구형 스타일 캐스트 문법으로는 절대 불가능하다.
      • 허나, 신경쓰일 정도로 런타임 비용이 높다는 단점이 있다.
    3. reinterpret_cast(n)
      • 포인터를 int로 바꾸는 등 하부 수준 캐스팅을 위한 연산자.
      • 결과가 구현 환경에 의존적이다.(즉, 이식성이 없다.)
      • 그러므로 하부 수준 코드 이외엔 거의 없어야 한다.(항목 50 참조)
    4. static_cast(n)
      • 암시적 변환을 강제로 진행할 때 사용한다.
        • 비상수 객체를 상수 객체로 변환
        • int를 double로 변환
      • 타입 변환을 거꾸로 수행할 때 사용한다.
        • void*를 일반 타입 포인터로 변환 
        • 기본 클래스의 포인터를 파생 클래스의 포인터로 변환

 

 

 

c++ 스타일 캐스트를 쓰는 게 바람직한 이유
  1. 코드를 읽을 때 알아보기 쉽다.
  2. 각 캐스트의 사용 목적 범위가 더 좁기 때문에 컴파일러 쪽에서 사용 에러를 진단할 수 있다.
    • 상수성을 없애려고 캐스팅 했는데 const_cast를 사용하지 않았으면 컴파일 에러가 발생한다.

 

 

 

타입 변환으로 인해 생성되는 런타임 실행 코드
int x, y;
...
double d = static_cast<double>(x) / y; // int -> double 캐스팅에서 코드가 생성된다.
  • 캐스팅 부분에서 코드가 생성된다.
  • 대부분의 컴퓨터 아키텍처에서 int의 표현구조와 double의 표현구조가 아예 다르기 때문이다.

 

 

 

캐스팅 이슈
  1. 객체 하나가 가질 수 있는 주소가 오직 한 개가 아니라 그 이상이 될 수 있다.
    • class Base { ... };
      class Derived : public Base { ... };
      
      Derived d; 
      Base *pb = &d; // Derived* -> Base*의 암시적 변환이 발생.
    • *pb와 &d의 값이 같지 않을 때가 가끔 발생한다!
    • 이는 C++에서만 가능한 상황이다.
    • 다중 상속이 사용되면 항상 이런 현상이 발생한다.
    • 즉, 데이터가 어떤 식으로 메모리에 박혀 있을 거라는 섣부른 가정을 피해야 한다.
      • 포인터 변위를 가정한 트릭은 위험하다.
      • 어떤 객체의 주소를 char* 포인터로 바꿔서 포인터 산술 연산을 적용한다는 식의 사용법은 미정의 동작.
  2. 보기엔 맞는 것 같지만 실제론 틀린 코드
    • class Window
      {
      public:
          virtual void onReisze() { ... }
          ...
      };
      
      class SpecialWindow : public Window 
      { 
      public: 
          virtual void onResize() 
          { 
              static_cast<Window>(*this).onResize(); // 동작이 안 된다.
              .... 
          } 
      };
    • 이 경우 예상처럼 동작하지 않는다.
    • 캐스팅 시 *this의 기본 클래스 부분에 대한 사본이 생기기 때문이다.
    • 그 사본의 onResize를 호출하므로 미정의 동작을 발생시킨다.
    • 결국 현재의 객체에 대해 Window::onResize()를 호출하지 않고 지나간다.
    • 반드시 아래와 같이 사용해야 한다.
      class SpecialWindow : public Window 
      { 
      public: 
          virtual void onResize() 
          { 
              Window::onResize(); 
              .... 
          } 
      }; 
      

 

 

 

dynamic_cast
  • 상당수의 구현환경에서 정말 느리게 구현되어 있다.
  • 허나 dynamic_cast를 쓰고 싶어지는 경우가 분명 생긴다.
    1. 파생 클래스 객체임이 분명한 객체에게서 파생 클래스 함수를 호출하고 싶다.
    2. 하지만 그 객체 조작 수단이 기본 클래스의 포인터(혹은 참조자)뿐이라면.
  • dynamic_cast를 피하는 방법
    1. 파생 클래스 객체에 대한 포인터를 컨테이너에 담아서 각 객체를 기본 클래스 인터페이스를 통해 조작할 필요를 없앤다.
      • class Window { ... };
        class SpecialWindow : public Window
        {
        public:
            void blink();
            ...
        };
        
        // 다음의 경우를 고쳐보자.
        typedef vector< shared_ptr<Window> > VPW;
        VPW winPtrs;
        ...
        for (VPW::iteractor iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter)
        {
            if (SpecialWindow* psw = dynamic_cast<SpecialWindow*>(iter->get()))
                psw->blink();
        }
        
        // dynamic_cast를 제거한 코드
        typedef vector< shared_ptr<SpecialWindow> > VPSW;
        VPSW winPtrs;
        ...
        for (VPW::iteractor iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter)
        {
            (*iter)->blink();
        }
      • 이 경우 하위 클래스 별로 타입 안전성을 갖춘 컨테이너가 여러 개 필요하다는 문제점이 있다.
    2. 원하는 조작을 가상 함수 집합으로 정리해서 기본 클래스에 넣어둔다.
      • 즉, 아무것도 안하는 기본 가상 함수를 기본 클래스에서 제공한다.
      • 가상 함수 기본 구현시 주의 (항목 34 참조)
      • class Window
        {
            virtual void blink() {} // 아무 동작 안하기.
            // 허나 가상 함수 기본 구현은 조심해야 한다.(항목 34 참조)
        };
        class SpecialWindow : public Window
        {
        public:
            void blink() { ... } // 구현
        };
        
        // 아래는 이제 모든 하위 클래스에 대응하여 동작한다.
        typedef vector< shared_ptr<Window> > VPW;
        VPW winPtrs;
        ...
        for (VPW::iteractor iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter)
        {
            (*iter)->blink();
        }
      •  

 

 

 

폭포식 dynamic_cast는 반드시 피할 것!
typedef vector<shared_ptr<Window>> VPW; 
VPW winPtrs; 
... 
for(VPW::iterator iter = winPtrs.begin() ; iter != winPtrs.end() ; ++iter) 
{ 
    // 폭포식 dynamic_cast
    if (SpecialWindow1 *psw1 = dynamic_cast<SpecialWindow1*>(iter->get())) 
    {
        ...
    } 
    else if (SpecialWindow2 *psw2 = dynamic_cast<SpecialWindow2*>(iter->get())) 
    {
        ...
    } 
    ...
}
  • 크기만 하고 아름답지 않으며 속도도 둔하고 망가지기 쉽다.
  • 파생 클래스가 추가될 때마다 조건 분기문을 추가해야 한다.

 

 

 

결론
  • 잘 작성된 C++ 코드는 캐스팅을 거의 쓰지 않는다.
  • 써야 할 때는 최대한 격리시키자.
    • 캐스팅을 해야 하는 코드를 내부 함수 속에 몰아 놓고, 그 안의 일은 공개하지 않는다.

 

 

 

참조
 

[More Effective C++] 2. C++ 스타일 캐스팅

항목 2 : 가능한 C++ 스타일의 캐스트를 즐겨 쓰자.   C 스타일 캐스트의 문제점타입을 다른 타입으로 제한 없이 바꾸어준다.어떤 객체의 상수성(copnstness)만을 바꾼다.기본 클래스 객체에 대한

delightlane.tistory.com

 

728x90
Comments