[cpp] C++ 가상 함수의 정렬 순서

@codemaru · March 09, 2011 · 5 min read

고급 언어를 사용하는 대다수 개발자들이 하는 가장 큰 착각은 고급언어와 최종 기계어가 1:1로 매핑된다는 상상이다. 쉽게 말해서 그냥 눈에 보이는 대로 믿는 것을 말한다. 대표적인 예가 최적화 이슈다. 많은 C언어 개발자들이 i=i+1보다는 ++i가 더 최적화된 코드라고 생각한다. 여기서 중요한 것은 그것이 진실인지 아닌지가 아니다. 단지 글자 수가 작기 때문에 더 최적화된 코드라고 생각한다는 그 과정이다. 삼항 연산자를 배운 C언어 개발자들은 다음과 같은 맛깔스런 코드를 쓰면서 자부심을 느끼곤 한다.

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

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

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

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

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

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

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

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;
};

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

@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중