C++ 이야기 여섯번째: 기본기 다지기(bool 타입에 관하여)

Posted at 2007. 4. 1. 18:06 // in S/W개발/C++ 이야기 // by 김윤수


[이글의 최신 Update 문서는 항상 여기에서 확인할 수 있습니다]

오랜 동안 posting 하지 않아 죄송합니다. 누가 뭐라 그러시는 분은 없었지만, 제가 괜히 맘에 찔리는 건 어쩔 수 없더군요. C++ 이야기 여섯번째입니다. 개발자가 아니신 분들이나 C++ 로 주로 개발하지 않으시는 분들은 별로 관심이 가는 내용이 아닐 것 같네요. 그렇다고 그런 분들께 해가 되는 내용은 아니니 한 번 읽어 보시는 것도 크게 나쁘진 않을 듯... ^^; (제가 보기에도 비굴하네요. 아무리 그래도 target audience를 늘리기 위한 근거 없는 말인 듯... ㅋㄷㅋㄷ. 그리고, 이 글은 만우절 글 아닙니다. 별 생각없이 포스팅하고 나서 메타블로그 확인했더니 만우절 글들이 막 올라오고 있네요. 덕분에 재밌는 글들 많이 읽긴 했지만... 이 글은 만우절 낚시글이 절대 아닙니다. 혹시라도 오해하는 분 있을까 봐 노파심에 밝힙니다.)

이번 글은 제목에 밝힌 바와 같이 C++의 아주 기본 중의 기본에 대한 것입니다. C++의 기본 타입 중 bool 이라는 타입에 관한 얘기입니다. 이 기본 중에 기본적인 내용에 뭐 할 말이 있겠느냐구요 ? 그러게요. 이 기본 중에 기본에 해당하는 부분에서 제가 할 말이 있다는 것이 저도 신기할 따름입니다. ^_^

우선 본격적으로 제가 풀어 놓고 싶은 이야기 보따리 중 첫번째는 C++의 기본 타입으로 bool 이라는 타입이 지원된다는 것입니다. C++의 첫번째 표준안인 98년 9월 1일 버전이 발표된 이후 계속해서 지원되는 타입입니다. 정말입니다. 못 믿으시겠다면, 제가 위에 링크를 달아놓은 문서의 3.9.1 Fundamental Types 의 6번, 7번 항목을 보시기 바랍니다. 어때요 ? 거짓말 아니죠 ?

98년 이후에 C++를 배우신 분들이나 이미 bool 타입을 지원해주는 컴파일러에서 C++를 배우신 분들은 제가 C++의 기본 타입으로 bool 타입이 지원된다는 걸 굳이 강조하는 것을 의아해 하실 분도 있겠네요. 98년 이전에는 bool 타입이 일부 컴파일러에 의해 지원되기는 했지만 98년 이후에서야 공식적으로 지원되기 시작했기 때문에 bool 타입이라는 것이 진정 portable 한 타입이 되었던 것이지요. 물론 표준안이 발표되고 나서 실제 컴파일러들이 그 표준안을 지원하기까지는 어느 정도 시간이 걸리기 마련이므로 진정한 portable 한 타입이 되려면 시간이 걸렸겠지만, 적어도 컴파일러 제작사에게 bool 타입을 지원하라고 강력하게 요청할 수 있었던 때였습니다.

한 가지 문제가 이상과 현실과의 차이에서 발생한다고 할 수 있는데요... 98년 이전에 개발되었던 소프트웨어 중에 bool 이라는 타입이 유용하다는 걸 발견하고는 나름대로 bool 이라는 타입을 typedef 로 정의해 놓고 쓸 수도 있었을 것이구요. 또는 98년 이전에 C++를 배운 C++ 개발자가 표준안에 bool 이라는 타입이 기본 타입이라는 걸 모르고, bool 이라는 타입을 typedef로 정의해 놓고 쓸 수도 있었을 것입니다. 또는, 자신이 현재 사용하고 있는 컴파일러가 C++ 98 표준안이 제정된 후에도 bool 타입을 지원하지 않아서 어쩔 수 없이 typedef(또는 #define) 로 정의해 놓고 쓸 수도 있을 것입니다. 이런 상황에 처한 분들의 해결책이 다음과 같았다면 어떤 일이 벌어질까요 ?

typedef int bool      // 또는
#define bool int

"글쎄... 별 문제가 없어 보이는데요 ?" 라고 반문하시는 분들이 있네요. 예, 그렇죠. 여기까지는 별 문제가 없어 보입니다. 어차피 위와 같이 정의한 분들도 나름 똑똑한 분들이었을테니 위와 같은 해결책을 썼을테죠. 예를 들면, bool 타입을 지원하는 컴파일러를 기준으로 돌아가는 S/W가 있었는데, 그 S/W를 자신의 컴파일러로 porting 하려다 보니 bool 타입을 지원하지 않아서 엄청난 컴파일 에러가 쏟아지는 걸 보고, 위와 같이 정의하셨을 수도 있을 겁니다.

예, 그렇담 다음 코드를 한 번 보시죠.

// myfunc.cpp 에 있는 내용
#define bool int
#define false 0
#define true 1

bool funcA(int a, bool b)
{
    ...                   // funcA 의 정의
    return true;
}

// main.cpp 에 있는 내용
bool funcA(int a, bool b);

int main(int argc, char* argv[])
{
    bool bRet = funcA(argc, true);
    ....                  // main() 함수의 로직
}

위 코드의 문제점이 보이시나요 ? 위 코드의 문제점이 첫 눈에 보이신다면 대단한 고수임에 틀림이 없으실 겁니다. 저야 뭐 문제를 낸 사람 입장이니 문제점이 한 눈에 보이는 건 당연하구요. 그렇다고 제가 고수란 건 아닙니다. 제가 우연히 위와 같은 문제를 경험해 보았기 때문에 알게 된 것이지요. 첫 눈에 안 보이시는 분들이 많을테니 생각할 시간을 좀 드리지요.

.


.


.


.


.


.


.


.


.


.


.


.


.


.


.


그럼 이 정도 시간을 드렸으면 알아내셨으리라 생각하고 코드를 자세히 들여다 보기로 하겠습니다.

문제는 bool 이라는 기본 타입을 제공하는 컴파일러에서 발생하게 됩니다. bool 이라는 기본 타입을제공하지 않는 컴파일러인 경우에는 아예 main.cpp 를 컴파일하지 못하므로 main.cpp를 컴파일할 때 소스를 뱉어낼 것입니다. 아주 기분 나쁘게 말이죠 ^^; (전 솔직히 컴파일러가 제 소스 컴파일 못하겠다고 뱉어내면 정말 기분 나쁘더군요. 조금 틀린 거 가지고 째째하게 뱉어내기는... 그냥 지가 대충 고쳐서 컴파일하지... ㅋㅋㅋ)

우선 myfunc.cpp 를 들여다 보죠. myfunc.cpp에 정의된 bool funcA(int a, bool b)의 함수 원형(prototype)은 C++ preprocessor에 의해 int funcA(int a, int b)로 바뀌게 될 것입니다. 그리고, 그에 맞게 코드가 생성될 것입니다. 한 번 g++ 3.4.4 버전(WinXP cygwin에서 실행함)에서 시험해 볼까요 ?

$ g++ -c myfunc.cpp
$ g++ -c main.cpp
$ g++ -o booltest myfunc.o main.o
main.o:main.cpp:(.text+0x39): undefined reference to `funcA(int, bool)'
collect2: ld returned 1 exit status


이게 무슨 에러인고 ...? 당연히 링크되어야 하는 거 아닌가요 ? 몇 몇 분은 눈을 O.,O 똥그랗게 뜨고 보고 계시는 게 눈에 선하네요. 이런 걸 컴파일 에러가 아니라 링크 에러라고 합니다. 링크 에러는 컴파일 에러보다 잡기가 더 힘들죠. 그럼 좀 더 깊게 들어가 보도록 하겠습니다. 링크 에러의 친구인 nm 유틸리티를 사용해 보도록 하겠습니다.

$ nm myfunc.o
00000000 b .bss
00000000 d .data
00000000 t .text
00000000 T __Z5funcAii

$ nm main.o
00000000 b .bss
00000000 d .data
00000000 t .text
         U __Z5funcAib
         U ___main
         U __alloca
00000000 T _main


myfunc.o 에 정의된 funcA 와 main.o 에 정의된 funcA 의 symbol 이름에 차이가 있는 것이 보이시나요 ? myfunc.o 에는 __Z5funcAii 로 정의되어 있고, main.o 에는 __Z5funcAib 로 정의되어 있습니다. 아까 말씀드린 대로 myfunc.cpp 를 컴파일할 때는 C++ preprocessor 에 의해 함수 원형이 int funcA(int a, int b)로 바뀌었을테니, 컴파일러가 symbol 이름을 생성할 때, funcA 뒤에 두 argument 의 타입을 뜻하는 ii를 붙였을 것입니다. main.cpp 를 컴파일할 때는 컴파일러가 undefined symbol 이름으로 funcA(int, bool)을 뜻하도록 funcA 뒤에 ib 를 붙였겠지요. i는 int 타입의 argument 를 뜻하고, b 는 bool 타입의 argument를 뜻합니다(이런식으로 symbol 이름을 정하는 규칙은 컴파일러가 나름대로 정할 수 있습니다. 표준에 의해 정해진 것이 아닙니다. g++ 에서는 이런 규칙을 사용하나 봅니다).

이런식으로 myfunc.o 에 정의된 funcA의 symbol 이름과 main.o 에서 참조하는 funcA의 symbol 이름이 다르므로 컴파일러가 링크시킬 수 없는 것은 당연한 이치입니다. (물론 그렇더라도, 저희 입장에서 생각해 보면 알아서 고쳐서 링크시키면 될 것이지... 째째하게 에러를 내 뱉느냐며 투덜거리는 건 할 수 있겠죠. ^^; 가르쳐 준 것만 할 수 있는 컴퓨터로서는 어쩔 수 없는 한계이니 이만 저희 본 주제로 돌아가겠습니다)

이렇게 코드가 간단한 경우에는 링크 에러를 비교적 쉽게 찾아낼 수 있을 것입니다. 그렇지만 코드가 상당히 대규모인 경우에는 이런 자그만 링크 에러도 정확한 문제를 찾아서 해결하기가 여간 만만치 않습니다. 문제점을 간단히 하기 위해 실제 코드와는 별개의 샘플을 예로 들었지만, 여러 사람이서 함께 개발하는 대규모의 C++ S/W인 경우 충분히 발생할 수 있는 현상이랍니다.

이런 문제가 발생하게 된 근본 원인이 무엇일까요 ? 컴파일러 잘못인가요 ? 아니면 사람 잘못인가요 ? 물론 컴파일러를 탓할 수도 있겠지만, 그건 컴파일러를 만드는 분들께 얘기할 것이고, 제 글의 target audience로 삼은 분들은 주로 C++ 개발자이므로 일단 사람 잘못으로 보도록 하겠습니다. 그럼 위 코드를 작성한 C++ 개발자가 뭘 잘못한 걸까요 ? 여기에서도 여러 가지 잘못이 있기는 하겠으나 좀 더 근본적인 걸 언급한다면 #define 문으로 C++ 기본 타입인 bool, false, true 등을 재정의한 것이 잘못이라고 하겠습니다. #define 으로 이런 reserved word(예약어)를 재정의하게 되면 컴파일러가 수행되기 전 단계인 preprocessing 단계에서 코드가 대치되어 버리므로(bool funcA(int a, bool b) 가 int funcA(int a, int b)가 되는 것) 컴파일러가 문제를 알아낼 수가 없게 됩니다. 그렇다고, typedef 또는 enum으로 bool, false, true 등을 재정의해도 될까요 ? 다음과 같이요

enum bool { false = 0, true = 1 };

물론 여러분이 행운아라면 컴파일러에서 위와 같은 코드를 본 순간 마치 못 먹을 걸 먹은 듯 뱉어낼 것입니다. 그렇지만 여러분이 덜 행운아라면 컴파일러가 조용히 컴파일을 해 버리겠지요. 그리고는 위 코드를 만난 다음부터는 bool 타입을 위와 같은 타입으로 처리해 버릴 것입니다. 저는 행운아인가 봅니다. 제 컴파일러인 g++ 3.4.4 버전은 아래와 같이 에러를 발생시키네요.

$ g++ -c myfunc.cpp
myfunc.cpp:3: error: expected identifier before "bool"
myfunc.cpp:3: error: expected unqualified-id before '{' token
myfunc.cpp:3: error: expected `,' or `;' before '{' token


portability를 행운에 맡기시는 게 좋을까요 ? 아니면 여러분이 직접 문제를 막으시는 게 좋을까요 ?그냥 reserved word 를 재정의할 생각일랑은 꿈에도 꾸지 마시기 바랍니다. 정하고 싶으시다면 다음과 같은 방식이 좋습니다.

#if defined (__cplusplus) && (__cplusplus >= 199707L)
typedef bool MY_BOOL
#define MY_FALSE false
#define MY_TRUE true
#else
enum MY_BOOL { MY_FALSE = 0, MY_TRUE = 1 };
#endif


위와 같이 대문자로 쓴다면 나중에라도 혹시 reserved word 와 겹칠일은 없을 것이고, 앞에 MY_ 라고 붙인 건 혹시 다른 사람이 정의하는 타입과 겹치는 걸 방지해 주기도 합니다. namespace 역할을 하는 것이지요. 물론 언어 자체에서 namespace를 지원하는 경우에는 앞에 namespace를 붙여주는 것도 방법일 것입니다. __cplusplus 는 표준 predefined macro 로 어느 C++ 컴파일러든지 정의하도록 표준화되어 있습니다. 그리고 __cplusplus 값이 199707L 이면 C++의 첫번째 표준안인 98년 9월 1일 버전을 따른다는 것입니다. 그러니 위에 제시된 복잡한 코드는 컴파일러가 C++ 표준안을 따르면 C++ built-in 타입인 bool 을 쓰겠다는 것이고, 그렇지 않으면 나름의 MY_BOOL 타입을 정의해서 쓰겠다는 의미가 될 것입니다. 그리고 실제 코드에서는 bool 이라는 타입은 전혀 쓰지 않고, MY_BOOL 이라는 타입만 쓰면, 컴파일러가 bool 을 지원하든 지원하지 않든 간에 portable 한 코드를 작성할 수 있는 것이겠지요.

별 시시콜콜한 것 가지고, 얘기가 많이 길어졌네요. 결국 제가 여기에서 주장하고 싶은 것은 다음과 같습니다.

1. bool 은 C++의 기본(built-in) 타입입니다.
2. 언어 자체의 reserved word 는 절대 typedef, enum, #define 등으로 재정의하지 맙시다.
3. (덤으로 주로 Unix 계열 OS에서) 링크 에러가 발생할 때는 우리의 nm 친구를 활용하세요.


위 세 가지 사항을 꼭 기억하신다면 주일에 같이 놀아달라는 아이들을 기다리게 한 보람이 있겠네요. 긴글 읽어 주셔서 감솨~드리고요... 혹시라도 잘못된 부분이나 관련된 다른 지식이 있다면 댓글로 남겨주신다면 더더욱 감솨하겠습니다. 그리고, 소프트웨어 관련된 저의 다른 글들도 참고로 읽어 보세요.

소프트웨어는 soft 해야 제 맛이다
Flexible한 S/W 작성하기
소스코드 복사의 위험성
C++ 이야기 첫번째: auto_ptr 템플릿 클래스 소개
C++ 이야기 두번째: auto_ptr의 두 얼굴
C++ 이야기 세번째: new와 delete
C++ 이야기 네번째: boost::shared_ptr 소개
C++ 이야기 다섯번째: 내 객체 복사하지마!
C++ 이야기 여섯번째: 기본기 다지기(bool타입에 관하여)