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

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

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

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

  • 인자를 reinterpret_cast<B*> 한 후, 함수에서 reinterpret_cast<D*>하면 => 같은 주소, 134515072, segfault ((두번째 값은 실행하는 플랫폼/C-runtime에 따라 다르다)) 이라는 매우 재밌는 결과가 나온다
  • 인자를 static_cast<B*> 혹은 (B*)로 캐스팅하고(결과가 같다), 함수에서 reinterpret_cast<D*> 하면 => 원래 주소 + 4, –1 3 이 나온다. 주소 +4는 32bit 플랫폼이라 그렇다. ((물론 alignment에 따라 바뀔 순 있다))
  • 인자를 reinterpret_cast<B*>, 함수에서 static_cast<D*> 하면 => 원래 주소 – 4, 이상한 값(…), segfault 가 일어난다. 가상 함수 테이블(vftbl) 주소가 망가진 말로(…).

등을 볼 수 있다.

이런 일은 사실,

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

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

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

Jinuk Kim
Jinuk Kim

SW Engineer / gamer / bookworm / atheist / feminist

Articles: 935

13 Comments

  1. reinterpret_cast는
    전 오히려 새로운 해석을 부여한단 느낌에서 잘 와닿았었는데요.
    일단 이 경우는 부모 클래스로 가는 거니 안써줘도 잘 돌지 않나요?

  2. Dish / 그러함

    ipkn / 부모 클래스에서 자식 클래스로도 가잖아?(함수 안에서) 그때는 필요하지
    근데 내가 생각한 ‘재해석’은 뭔가 변화가 있는건데 가시적인(?) 변화는 없지(?)

  3. reinterpret_cast 테스트 결과 중에 이해 안 되는 부분이 좀 있긴 한데,
    특히 세번째( “인자를 reinterpret_cast, 함수에서 static_cast 하면”)에서 원래 주소가 나와야 하는 거 아닌가요?

    • reinterpret_cast<B*> 하면 원래 변수의 주소가 전달되고,
      static_cast<D*> 하면 해당 포인터 주소 – 4 를 해버려서 원래 주소가 아니라 괴악한 주소값(덕분에 가상 함수 호출이 segfault)으로 가버립니다.

      • kane / 그렇네요. 포인터 찍는 것에도 캐스팅하도록 수정했습니다.
        가지고 있는 코드는 그랬지만(…) 예제만들면서 그 부분은 제대로 못 넣었네요.

        ps. 근데 아이디가 낯이 익어서 해보는 소린데(…) SNAGs 란 곳을 아시나요?(…)

Leave a Reply