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

프로그래밍 2007.06.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


[기사] "내 꿈은 골방에 처박힌 프로그래머"

생각하다 2007.05.16 21:45
기사링크 : http://zine.media.daum.net/mega/h21/200705/15/hani21/v16737605.html

확실히 요즘 아이들(나도 여기에 반쯤은 포함된다 싶다)은 문제다. 사람과의 소통이 너무나도 단절되어 있다. 오죽하면 학원 때문에 죽겠네 뭐하네 하는 아이들이 친구들을 만나고 싶어 학원에 다니겠는가? (그조차도 피곤해서 잘 놀지도 못한다더라) 요즘들어 사이버공간에 대한 이슈, 뉴스를 많이 접하는 것 같다. 특히나 실생활(사생활, 사회)에 관련된 뉴스들.

뉴스를 접할 때마다- 어째서 사람들은 사이버 공간에 그렇게 큰 의미를 부여하려는 것인가, 싶다. 아니 실제로는 사람들이 왜 그렇게 생각하는 것인지가 궁금하다. 인터넷은 그저 매체인가? 전공자이기 때문에 그 관심은 더하다. 내가 볼 때 인터넷은 도구다. 싸이월드 역시 자기표현의 매체에 불과하다. 안락한 만남의 장이라는 것도 결국엔 일시적인 사람들간의 연결에 불과하지 않나? 본질은 사이버 공간이 아니다. 인간과 인간의 연결 그 자체라는 거다. 하지만 정말 인터넷이란 그 자체로 다른 공간인가?

실존하지 않는 사이버 공간을 다뤘던 옛 SF들이 생각나는 밤이다.



Trackbacks 0 : Comments 1
  1. Favicon of http://ninetail.wo.tc BlogIcon 나인테일 2007.05.18 20:57 신고 Modify/Delete Reply

    저런 친구는 한 일주일 게임회사에 집어 넣고 월화수목금금금으로 야근을 시켜 봐야 정신을 차릴겁니다....

Write a comment