Quiz: C++ casting 연산자

사실 오늘의 삽질 이라고 제목 붙여야 하지만(?). 아래와 같은 클래스 계층이 있다고 치자.

struct B {
    int b_x;
    B() : b_x(-1) {}
};
struct D : public B {
    D() : B() {}
    virtual int Get() const = 0;
};
struct C : public D {
    virtual int Get() const { return 3; }
};

이제 아래 코드를 실행한다고 하자.

C* c = new C();
printf("%8p %d %d\n", c, c->b_x, c->Get());

띄워놓은 터미널에서 실행시킨 결과,

0x8a2d008 -1 3

가 나왔다. 다른 운영체제/하드웨어에서 해봐도 주소값 바뀌는 거 빼곤 비슷한 결과가 나온다.

여기서 문제, 여기서 printf 부분을 다음과 같은 함수로 빼냈다.

void func(B* b) {
    printf("%8p %d %d\n",
        /* some casting */ b,
        b->b_x,
        /* some casting */ b->Get());
}

// ...
// in main
func( /* some casting */ c );
// ...

이 부분이 처음 했던 것처럼 동작하려면, 어떻게 캐스팅 해야할까?

물론 이 문제 자체는 굉장히 특이한 상황에서 – 특히나 C/C++이 섞여있고, 콜백등으로 인해서 C 타입이 왔다 갔다하는 경우 – 나 보게 되긴 한다. 그렇지만 이 문제 자체는 C++ 타입 변환을 이해하는데 도움이 된다.

그래서 후보를 골라보면,

  • C-style 캐스트. (D*)로 처음 코드에서 처럼 변환
  • static_cast<D*>: C++에서 연관관계(pointer 내지는 암시적인 변환으로 호환되는)가 있는 타입 간에 컴파일 시간에 검사하여 (실행시간에는 안정성 검사없이) 변환하는 연산
  • dynamic_cast<D*>: C++에서 상속 관계를 따라 실행 시간에 검사해서 타입 변환하는 연산
  • reinterpret_cast<D*>: 아무런 해석없이 있는 그대로 바꿔주는 변환연산.
  • const_cast<D*>: 상수성(const)나 volatile을 제거하려는 목적으로 쓰임. 여기선 불가능하지만 일단 리스팅.

들이다. 이걸 써서, 혹은 양쪽에 서로 다른 캐스팅을 적당히 조합해서 어떻게 써야, 처음 봤던 결과를 func를 호출해서도 볼 수 있을까?


답은,

C-style cast 와 static_cast<D*> 다.1 이유를 설명하자면,

우선 const_cast 는 위에서 써 놓은 이유대로 여기서는 아무런 의미가 없다.

다음으로 dyanmic_cast. B 타입이 가상 함수(virtual function)을 갖지 않기에(=다형성이 없기에) dynamic_cast 자체가 불가능하다.

reinterpret_cast의 경우엔 상당히 흥미로운데, 이건 “아무런 해석(=변경)“을 하지 않는다.그래서, 위에서 실행한 머신에서 해보면,

  • 인자를 reinterpret_cast<B*> 한 후, 함수에서 reinterpret_cast<D*>하면 => 같은 주소, 134515072, segfault2 이라는 매우 재밌는 결과가 나온다
  • 인자를 static_cast<B*> 혹은 (B*)로 캐스팅하고(결과가 같다), 함수에서 reinterpret_cast<D*> 하면 => 원래 주소 + 4, –1 3 이 나온다. 주소 +4는 32bit 플랫폼이라 그렇다.3
  • 인자를 reinterpret_cast<B*>, 함수에서 static_cast<D*> 하면 => 원래 주소 – 4, 이상한 값(…), segfault 가 일어난다. 가상 함수 테이블(vftbl) 주소가 정상적이지 않아서.

등을 볼 수 있다.

이런 일은 사실,

  • 가상 함수 때문에 숨겨진 vftbl 포인터 크기가 포함되어버리고,
  • 이에 대한 조절을 연관 타입 변환을 담당하는 static_cast나, 이를 쓰게 되는 C-style 캐스팅은 주소 변환이 제대로 이뤄지지만, (주소 +- 테이블 크기를 수행)
  • 값을 있는 그대로 보내주는 reinterpret_cast는 이를 처리하지 않아서 저런 괴악한 결과를 내는

것이다. reinterpret_cast 가지고 실험한 것은, 가상함수 테이블 위치를 잘못 잡았을 때 / 구조체 시작 주소를 잘못 계산할 때로 구분해서 해석하면 된다.

C++ 캐스팅 이젠 좀 이해가 되시는지?


  1. 사실 C-style cast는 일련의 C++ 캐스팅을 순서대로 (컴파일 시점에서) 시도해보는 것 뿐이다. 여기서는 그 순서에 따라 static_cast가 선택된다. 순서 자체는 stackoverflow.com의 이 답변을 참고하자. 덤으로 저 글에는 C++ 캐스팅 자체의 용법도 잘 설명되어 있다. ↩︎

  2. 두번째 값은 실행하는 플랫폼/C-runtime에 따라 다르다. ↩︎

  3. alignment에 따라 바뀔 순 있다. ↩︎