기본기 다지기, bool type에 관하여

이번 글은 제목에 밝힌 바와 같이 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 친구를 활용하세요.

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

Add a New Comment
or Sign in as Wikidot user
(will not be published)
- +
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License