본문 바로가기

[ IT Column ]/Programming

신입 개발자를 위한 조언 - C++ 가상 함수의 정렬 순서

고급 언어를 사용하는 대다수 개발자들의 가장 큰 착각은 고급 언어와 최종 기계어가 1:1로 매핑된다는 상상이다. 쉽게 말해서 그냥 눈에 보이는 대로 믿는 것을 말한다. 대표적인 예가 최적화 이슈다.

많은 C 언어 개발자들이 I=I+1보다는 ++i가 더 최적화된 코드라고 생각한다. 여기서 중요한 것은 그것이 진실인지 아닌지가 아니다. 단지 글자 수가 적기 때문에 더 최적화된 코드라고 생각한다는 그 과정이다. 삼항 연산자를 배운 C 언어 개발자들은 다음과 같은 맛깔스런 코드를 쓰면서 자부심을 느끼곤 한다.

meaning = flag ? "on" : "off";

그러면서 다음과 같이 if 문을 쓰는 개발자들을 ‘오스트랄로피테쿠스’ 취급하는 것 또한 잊지 않는다.

if(flag) meaning = "on";
else meaning = "off";

하지만 그런 그들도 경험이 쌓이고 연륜도 생기고 시야가 넓어지고 마음도 유해지고 하다 보면 이런 것들이 얼마나 부질없는 생각이었는지를 배운다. 그 즈음이 되면 더 이상 고급 언어와 최종 단계의 결과물 사이에는 그렇게 큰 연관성이 없다는 것을 알게 되고, 더 이상 보이는 것에 현혹되지 않아야겠다는 생각을 한다. 하지만 그렇게 굳게 마음을 먹어 보지만 보이는 것에 혹하지 않기란 생각보다 쉽지 않다.

<리스트 1>과 같은 구조체의 확장 방식은 C 언어에서 널리 사용되는 기법이다. V1 구조체를 V2 구조체로 자연스럽게 확장해 나가는 이 방식은 군더더기가 없고 확장된 함수를 만들기도 쉽고 관리하기도 편하다는 장점을 가진다. 윈도우의 수많은 API들도 이러한 방식으로 95에서 98로, 2,000에서 XP로 구조체를 확장해가며 기능을 늘려왔다. 이런 방식을 사용할 수 있는 가장 큰 근간은 바로 구조체의 멤버가 우리가 바라보는 그것과 완전히 일치한다는 점에 있다.

그렇다면 <리스트 2>와 같은 가상 함수의 정렬 순서는 어떨까? 과연 저 함수들도 구조체와 동일한 방식으로 매핑될까? 결론만 말하자면 가상 함수의 정렬 순서는 눈에 보이는 그것과는 다르다. 비주얼 스튜디오는 위 예제에 해당하는 가상 함수들을 다음과 같이 배치한다. IV1의 경우는 M1(int a)은 0번째에, M1()은 1번째에 위치시킨다. IV2의 경우에는 M1(int a, int b)가 0번째에, M1(int a)가 1번째에, M1()이 2번째에 배치된다. 이 말이 의미하는 바는 이 녀석들이 바이너리 단계에서는 전혀 호환되지 않는다는 것이다. 즉, IV1으로 컴파일된 모듈에 IV2의 포인터를 집어넣으면 크래시가 난다는 소리다. 이 경우에 원래 의도했던 형태로 작성하기 위해서는 C++의 상속을 이용하는 것이 바람직하다.

<리스트 1> 구조체의 확장 방식

typedef struct _V1
{
    size_t size;
    int first;
} V1; 

typedef struct _V2
{
    size_t size;
    int first;
    int second;
} V2;

<리스트 2> 가상 함수의 정렬 순서

class IV1
{
public:
    virtual void M1() = 0;
    virtual void M1(int a) = 0;
}; 

--------------------------------------------------------------------------------------------------------------------------

class IV2
{
public:
    virtual void M1() = 0;
    virtual void M1(int a) = 0;
    virtual void M1(int a, int b) = 0;
};

 

- Reference

  http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&wr_id=36903