C++ 추상 클래스와 포인터 캐스팅, 다중상속 ㅅㅂㄹㅁ

프로그래밍 2007. 6. 13. 22:41
상황설명

A라는 부모 클래스를 만들고, 이 A라는 클래스의 포인터인 A* pA 를 인자로 하는 foo(A* pA)를 정의해서 사용중이었다. 이 함수의 인자로는 A를 상속한 B나 C클래스를 예상할 수 있으며, 이 B와 C클래스는 I라는 순수 가상함수를 상속한 다중상속 객체들이었다. I는 순수가상함수인 Vfoo()를 가지고 있었다. C는 생성자에서 A의 함수(가상함수가 아니다)를 호출한다.

사용자 삽입 이미지

이런 상속관계 (수정)


vector<I*> vt;

...

C* pC = new C;
I* pI = (I*)pC; // C* -> I*
vt.push_back(pI);

...

A* pA = (A*)vt[0]; // C* -> I* -> A*
foo(pA);

...

foo(A* p_pA)
{
    I* pI = (I*)p_pA; // C* -> I* -> A* -> I*
    pI->Vfoo(); // 여기서 문제발생
}

문제발생

문제는 C형 인스턴스를 가리키는 A형 포인터 pA_dir_C를 foo(A* pA)의 인자로 넘겨주면서 일어났다. foo(A* pA)함수 내부에서는 인자로 받은 pA를 (I*)pA 로 형변환하여 Vfoo()를 호출하는 루틴이 들어있었는데, 여기서 에러가 발생한 것이다. (6025 pure virtual function call)

Watch를 통하여 살펴보니 vftable의 문제였는지, Vfoo()가 아닌 엉뚱한 함수가 불려오더라. (다른 추상 클래스 I2에서 오버라이딩 된 가상함수였다.) 포인터 형변환에서 가상함수 테이블에 변화가 오는 것일까?

해결책

결국 C* -> I* -> A* -> I* 와 같은 여러 번의 형변환을 거치지 않고 C* -> I* 상태에서 pI->Vfoo() 식으로 형변환 단계를 줄여서 사용하였다. 정확히 말하면 문제를 해결한 것이 아니라 회피한 것이나 마찬가지다. 하지만 적당한 레퍼런스도 없고, 도와줄만한 사람도 없고 해서. 코드 길이는 좀 길어질지 몰라도 저런 식으로 함수를 통하는 단계를 줄여버렸다.

감상

쩐의 전쟁이 너무 재밌다.


Trackbacks 1 : Comments 11
  1. ProgC 2007.06.14 02:24 Modify/Delete Reply

    음... 추상화 인터페이스를 이용한 설계를 하시는거 같은데
    그림이 좀 잘못된 것 같습니다. 그림상으로 보면 I인터페이스가 B, C를 구현하는걸로 되어 있네요. 그 반대가 되어야 할 것 같습니다.

  2. 동숙이 2007.06.14 12:23 Modify/Delete Reply

    정확한 소스를 어떻게 되나요?
    궁금해서 코드를 만들어 봤는데..

    제가 한봐로는 문제가 없는거 같은데요.

    • Favicon of https://axnoah.tistory.com BlogIcon AxNoah AxNoah 2007.06.14 18:54 신고 Modify/Delete

      그럼 저 자체로는 문제가 없다는 걸까요.
      6025 pvfc 문제는 생성자에서 가상함수 테이블 관련해서 일어나는 문제라고 하던데, 형 변환 과정에서 가상 테이블의 변화가 있는지 없는지를 몰라서요.

  3. Favicon of http://dcple.com BlogIcon chadr 2007.06.14 14:59 Modify/Delete Reply

    제가 봐도 코드상에 이상한점은 안보이는군요..

    저기 안보이는 "...."부분에서 뭔가 잘못된 일을 하지 않는가 싶군요.

  4. Nagne 2007.07.31 11:44 Modify/Delete Reply

    검색으로 지나가다가 소스를 보고 발목잡혀 글남깁니다...
    일단 소스는 좀 오류가 심각하네요....당연히 않됩니다...
    I* pI = (I*)pC; 이부분과
    vt.push_back(pI); 이부분과
    A* pA = (A*)vt[0]; 이부분이 잘못됐습니다..

    이부분이 문제 입니다.... 인터페이스 I와 단순한 부모클래스 A를 다중상속받아서 사용하고 있습니다...
    벡터에 저장할 때는 (I*) 형으로 변환하고 있군요..
    자 일단 다중상속 받는 순서가 I 가 먼저오고, A가 다음에 온다고 가정하겠습니다...
    pC 의 주소값이 0x1000 이라고 가정하구요....
    I* pI = (I*)pC;
    A* pA = (A*)pC;

    이렇게 했을때 pI의 값은 0x1000입니다..
    그리고 pA의 값은 0x1000+alpa 입니다...
    왜 그러냐구요...클래스 포인터의 형변환은 주소값을 바꾸는
    기능을 합니다....(short*) 를 (int*)로 형변환 하는것하고
    클래스의 포인터를 형변환 하는것은 아주 큰 차이가 있습니다.
    (short*)를 (int*)로 형변환 하는것은 reinterpret 캐스팅이라고합니다. 형변환한 주소값에 아무런 변화가 없습니다. 형변환 하지 않을 경우 컴파일러가 이부분 문제가 있어보인다며 확인할것을 강요하고 프로그래머가 형변환 코딩을 넣도록 강요하는 역할을 가집니다.

    그러나 클래스 포인터의 형변환은 완전히 다른 의미입니다...즉 해당 클래스로 형변환 하면서 주소를 계산을 하는것이죠....이때에 룰이 있습니다..
    두 클래스간에 상속관계가 전혀 없을때는 주소계산을 하지 않습니다. 이것도 reinterpret 캐스팅이라고 합니다.. 그리고 상속관계에 있을때는 포인터 형변환시에 static 캐스팅이 발생합니다..주소값을 계산해 버리죠,....

    I* pI = (I*)pC; // 여기서 static 캐스팅 됐습니다..

    C는 I를 먼저 상속받았다고 가정했으니..static 캐스팅했지만 값의 변화는 없을겁니다...
    만약 (A*)pC 하였다면 값의 변화가 옵니다.

    그런데...
    (A*)vt[0]; 요 부분에서 vt[0]의 형은 (I*) 형입니다. 그리고 I와 A는 아무런 연관성이 없습니다..
    때문에 여기서는 reinterpret 캐스팅이 발생합니다...
    즉, A* pA = (A*)vt[0]; 요 부분에서 pA가 정상적인 동작을 할려면 0x1000+alpa 로 저장이 되어야 하는데, reinterpret 캐스팅 되어서 pA에 저장된 주소값은 0x1000이 되어버린것입니다...그러면 pA를 이용해서 Vfoo()를 호출하면 I의 영역을 A의 영역처럼 사용하겠죠..숨은 버그입니다... 그리고 c++에서는 static_cast<> 연산자와 reinterpret_cast<> 연산자가 있어서 위와 같은 버그를 컴파일 타임에 잡아줄수 있게끔 하고 있습니다.. (I*) 와 (A*)는 C 타입의 형변환입니다..reinterpret 캐스팅과 static캐스팅의 구분이 엄격하게 따지고 들어야만 제대로된 코딩이 가능하죠....
    아 그리고
    foo() 함수 내부에서
    * pI = (I*)p_pA; 요부분도 reinterpret 캐스팅이죠...아무연관성 없는 포인터형으로 변환한 것이니깐요..

  5. Nagne 2007.07.31 12:07 Modify/Delete Reply

    위 코드를 수정한다면 다음과 같이 하면 될겁니다..

    vector<I*> vt;

    ...

    C* pC = new C;
    I* pI = static_cast<I*>pC; // C* -> I*
    vt.push_back(pI);

    ...

    A* pA = static_cast<A*>static_cast<C*>vt[0]; // C* -> I* -> C*-> A*
    //I와 C는 연관성이 있지만 I와 A는 연관성이 없기에 연관성 있는것으로 중간에 연결가능한 C를 거쳤다가 변환합니다..당연히 static_cast이어야 합니다..

    foo(pA);

    ...

    foo(A* p_pA)
    {
    I* pI = static_cast<I*>static_cast<C*>p_pA; // C* -> I* -> C* -> A* -> C* -> I*
    //마찮가지로 A* 에서 I*로 형변환 하는 사이에 C*로 정적 캐스팅합니다..
    pI->Vfoo(); // 이제는 정상동작
    }


    static_cast가 하는일은 베이스클래스로 형변환 할때는 차일드 클래스의 주소공간 중에서 베이스클래스의 주소를 찾아서 주소값을 바꿔주고, 차일드 클래스로 형변환 할때는 베이스클래스주소가 그 차일드 클래스로부터 정적 캐스팅하였을때 획득한 주소라고 가정하고 차일드의 주소를 계산하여줍니다...
    따라서 사실 위 소스코드는 정상 동작하지만...
    vector<I*> vt 가 B 클래스의 인스턴스의 주소도 같이 저장하는 벡터라면 여전히 버그가 존재한다는것 잊지마세요...
    일단 버그 상관 없이 동작시킬라면 B 하고 C가 I하고 A를 상속받는 순서를 똑같이 하여야만 하고, B저장했던것인지 C저장했던것인지 확실히 구분 가능하여야만 위 코드가 정상 동작할 겁니다...

    class I{}; class A{}; class X{}; class Y{};
    class B : public I,public A ,public X{};
    class C : public A,public I ,public Y{};

    만약 클래스 구조가 이딴식으로 된다면....위에 수정해 드린 코드로도 정상동작은 않됩니다...단 vt[0]가 B의 인스턴스로부터 저장된 것인지. C의 인스턴스로부터 저장된것인지 확실히 알수 있다면 위와 같은 방식의 형변환으로 충분할 것입니다...
    그럼 이만...

    그리고 더 궁금한거 있으면 yilove78@fme.co.kr 로 문의하세요....

  6. Favicon of http://wafe.kr/ BlogIcon wafe 2007.08.28 15:42 Modify/Delete Reply

    다중 상속에 대해서 검색하다 우연히 들렀는데, 이미 다른 분께서 해결책을 남기셨군요. ^^

    C++에서는 타입 캐스팅을 할 때 C 스타일의 캐스팅보다는 C++ 스타일의 static_cast, dynamic_cast, reinterpret_cast 를 쓰는 것을 추천하고 있습니다.

    C의 캐스팅에 비해 여러가지 의미가 첨가되어 있는 것이 C++의 캐스팅이기 때문에 캐스팅의 내용을 좀 더 명확하게 알고 구분해서 쓰라는 의미가 있고요, 또 글에 쓰신 것과 같이 부적절한 캐스팅에 대해서 컴파일러가 힌트를 줄 수 있기 때문입니다.

    • Favicon of https://axnoah.tistory.com BlogIcon AxNoah AxNoah 2007.09.15 12:50 신고 Modify/Delete

      관련 글 들은 몇 번 읽은 적이 있는데, 아무래도 습관 탓인지 아직 익숙하지 않네요. 경험부족이 이런데서 나오나 ㅠㅠ 음. 컴파일러가 알려 줄 수 있으면 적극 활용해야겠네요.

Write a comment