printf/scanf가 받는 % 문자는 이식성 면에서 매우 큰 문제를 일으킬 수 있다. 기계 종류와 운영체제/컴파일러(정확하게는 CRT 라이브러리)의 종류에 따라 미묘한 차이가 존재하기 때문이다.
이런 잡음이 제일 없던 꿈 같은 시절은 단연 32비트 시절이다. 포인터와 정수가 전부 4바이트가 됨으로써 %d와 %ld 같은 골치아픈 구분도 없어졌고, 포인터도 far/huge 같은 구분이 없어져서 모든 것이 32비트 단위로 끝이 났기 때문이다. %d, %x, %u 하나만으로 컴퓨터에서 통용되는 거의 모든 정수를 바로 읽고 쓸 수 있던 시절. -_-
* * * * * *
Note 1
참고로 정수가 아닌 실수는?
16비트 시절에는 터보 C/C++에 무려 10바이트 크기의 실수인 long double이 있었고, 파스칼에는 아예 6바이트짜리 Real이라는 기괴한 실수가 존재했다. CPU의 machine word가 16비트 크기이고, GPU는커녕 부동소숫점 전용 프로세서(FPU)마저 흔치 않아서 이런 연산도 소프트웨어적으로 직접 구현하는 게 당연시되던 시절이었으니까 그런 게 존재 가능했다.
요즘 세상엔 무조건 32비트 float 아니면 64비트 double이지, 저런 건 상상도 못 할 개념일 것이다. 픽셀 크기조차도 옛날에는 트루컬러 24비트이다가 요즘은 컴퓨터가 더 처리하기 편한 형태인 32비트이다.
* * * * * *
하지만 이렇게 32비트 천하통일 시대에 끝이 보이기 시작한 것은, 64비트 컴퓨터가 속속 등장하고 문자열도 일반적인 8비트 크기가 아닌 16비트 단위의 소위 wide string이 공존하게 되고부터이다.
그럼, 이번에도 역시 숫자부터 예를 들어 보겠다.
32비트 윈도우 + 비주얼 C++의 CRT는
32비트 정수를 주고받을 때는 당연히 그대로 %d나 %u를 주면 되고 별도의 크기 지정자가 필요 없다. 하지만 64비트 정수에 대해서는 I64라는 접두사를 넣어서 %I64d처럼 해야 한다.
이 규칙은 64비트에서도 완전히 동일하게 적용되기 때문에 이식이 쉽다.
특히 호환성을 극도로 중요시하는 윈도우는 64비트 기계에서도 int 형을 32비트 4바이트로 책정한 관계로, 64비트에서도 %d가 아닌 %I64d를 해 줘야 32비트 영역을 넘어서는 정수를 읽거나 쓸 수 있다. 64비트 기계이더라도 숫자는 일단 변함없이 32비트가 주류라는 인상을 넣은 것이다.
* * * * * *
Note 2
윈도우즈 문화권은 왜 이리도 호환성에 목숨을 걸까?
간단하다. 그쪽은 오픈소스 진영과는 근본적으로 분위기가 다르기 때문이다.
모든 것이 투명하게 소스 공개이고, 사용자들이 다 컴퓨터를 능수능란하게 다루는 능동적인 해커들인 세상에서는 뭔가 소프트웨어를 업데이트해도 줘도 못 먹는 사람이 없이 물갈이도 금방 된다. 소프트웨어 계층에 breaking change가 잦더라도 재컴파일 한 번으로 '끗'이며, 그렇게 문제가 되지 않는다.
하지만 여기는 사정이 다르다. 마우스로 느릿느릿 아이콘 클릭밖에 못 하고 악성 코드에 속수무책으로 당하는 컴맹도 많다. 또한 돈 내기 싫어서 구닥다리 OS를 계속 고집하는 사람도 많다. 오로지 MS라는 회사가 모든 내부 사정을 관장하고 고객을 다 떠먹여 줘야 한다. 그러니 무조건 한번 만들어 놓은 것에 대한 유지 관리가 편리한 시스템을 만들어야 하며, 새 제품을 단절 없이 많이 팔려면 하위 호환성이라는 보수적인 가치를 최우선 순위로 두고 목숨 걸 수밖에 없는 것이다.
* * * * * *
단, 딱 하나 문제가 될 수 있는 것은 소위 INT_PTR 타입으로, 32비트 기계에서는 32비트이지만, 64비트에서 실제로 64비트 크기로 확장되는 정수이다. 이게 진짜로 포인터의 크기와 같으며 machine word와 크기가 일치함이 보장되는 정수이다.
이런 정수를 다루는 프로그램의 이식성을 위해서 %Id가(64만 빼고) 별도로 추가되었지만, 이건 반대로 구형 CRT에서는 지원되지 않는 걸로 알고 있다.
그래도 다행인 건, binary format이 아니라 사람이 읽을 수 있는 문자열 형태로 숫자를 읽고 쓰는데 32비트 크기를 넘어서는 범위를 다루는 일은 굉장히 드물다는 것. 차라리 실수를 다루면 다뤘지 정수가 그러는 일은 드문 편이다.
참고로, 가변 인자 함수가 호출될 때, 모든 정수형은 기본적으로 int 형으로 promote가 일어난다. char이든 short이든 다 32비트 내지 64비트 크기로 폭이 증폭된다는 것이다. float는 double로 바뀐다. 그렇기 때문에 float나 double이나 동일하게 %f나 %g로 출력 가능하다. 단지, 값이 아니라 포인터가 전달되는 scanf를 호출할 때는, float에 대해서는 %f를, double에 대해서는 %lf라고 반드시 타입 구분을 엄격히 해 줘야 할 것이다.
64비트 정수를 전달할 때는 32비트 기계에서는 스택에 push가 두 번에 걸쳐 일어나지만, 본디 64비트이던 기계에서는 역시 한 번만에 인자 전달이 끝난다. 그렇기 때문에 %d %d %d 해 놓고 실제로 32, 64, 32비트의 순으로 변수를 전달했다면 32비트 기계에서는 마지막 숫자가 꼬이겠지만(push는 128비트, 하지만 pop은 96비트) 64비트는 둘째 정수가 범위만 32비트 내부에 있다면 세 숫자가 모두 제대로 출력이 된다(push와 pop 모두 64*3비트 동일).
물론 이 경우, 둘째 %d를 %I64d로 해 줘야 32와 64비트 기계에서 모두 잘 동작하는 portable 코드를 만들 수 있다.
윈도우 외의 다른 운영체제는 사정이 어떤가 모르겠다. 64비트 정수를 출력할 때 32비트 기계에서는 %lld, 심지어 64비트에서는 %ld 이렇게 차이가 존재한다고도 하는데.. =_=;;
gcc 자체가 I64와는 다른 관행을 사용하는데 기계마저 64비트로 가고 나면 이거 이식성 면에서 재앙이 발생하지는 않으려나 우려된다.
다음으로 문자열이 있다.
알파벳 이외의 문자는 다룰 일이 없는 서버나 게임 엔진 같은 아주 특수한 프로그램을 개발하는 게 아니라면 이제 유니코드 문자는 대세가 되어 있다. 물론 UTF8도 쓰이고 유닉스 계열 운영체제에서는 심지어 UTF32도 쓰이지만, 그래도 유니코드 문자열을 컴퓨터 메모리 상으로 저장하는 데 비용 대 효율이 가장 뛰어난 방법은 UTF16이다. 특히 윈도우는 NT 시절부터 이렇게 16비트 단위의 wide string을 내부적으로 다뤄 와서 wchar_t = 곧 unsigned short나 다름없을 정도이다.
printf는 ansi 버전과 wide 버전이 존재하며, format으로 지정해 주는 문자열과 %s로 전달하는 문자열의 타입은 대체로 일치한다. ansi 버퍼에다가 wide string을 출력한다거나 그 반대로 해야 하는 경우는 드물다. 하지만 그런 일이 아주 없는 것은 아니다.
이럴 때 윈도우에서는 %hs, %ls라는 지정자를 주어 h는 버퍼 크기와 상관없이 무조건 ansi, l은 버퍼 크기와 상관없이 무조건 wide라고 지정을 할 수 있게 했다. gcc 쪽은 잘 모르겠다.
그래서 함수 오버로딩이 지원되는 C++ 스트림이 짱이라는 말이 있을 정도이니까. 아무 곳에서나 그저 무조건 OK.
Posted by 사무엘