« Previous : 1 : ... 4 : 5 : 6 : 7 : 8 : 9 : Next »

오늘날 쓰이고 있는 컴퓨터라는 기계의 이론적 근간을 마련한 사람으로는 앨런 튜링(영국)이라든가 폰 노이만(헝가리->미국) 같은 불세출의 천재 수학자가 있다. 그런데 영국에서는 튜링보다 먼저, 범용적인 계산 기계라는 개념을 떠올렸던 천재가 있었으니 바로 찰스 배비지(1792-1871)이다. 전산학도라면 배비지의 해석 기관에 대해 들어 보지 못한 사람이 없을 것이다.

그는 시대를 너무 앞서 간 괴짜 덕후였으며, 재정 부족과 당대의 기계 제작 기술의 부족 때문에 그의 꿈이 당장 완전히 실현되지는 못했었다.
참고로 창조 과학회에서는 배비지가 독실한 크리스천 과학자였다고 띄우고 있다. ㅋㅋ 링크를 소개한다. (그 반면 튜링은 동성애자인 데다 자살로 생을 마감했으니, 기독교 진영에서는 별로 좋아할 구석이 없겠다)
http://www.kacr.or.kr/library/itemview.asp?no=243

그런데, 이 시대에 영국에서는 배비지의 계보를 이을 천재 수학자가 또 태어났는데, 이번엔 여자였다. 그녀의 이름은 에이다(Ada Lovelace; 1815-1852). 인류 역사상 최초의 프로그래머라고 일컬어지는 먼치킨 엄친딸 공순이이다. 이 두 사람은 사진기가 발명되기 거의 직전의 시기를 살았던 사람인지라 사진은 전해지지 않으며, 초상화나 스케치로 그려진 모습만 전해 내려온다. 하지만 에이다는 상당한 미녀였다고 한다. Lovelace라는 성은 백작 작위를 얻은 남편의 성에서 딴 것.

에이다는 ‘자고 일어나 보니 유명해졌더라’ 같은 어록으로 유명한 시인 바이런의 외동딸이었다. 아버지가 희대의 바람둥이었다고 하는데 딸 역시 머리가 비범했고 나중에는 도박에 빠졌다고 하니 평범한 인생을 살지는 못했으며, 그러다 자궁암 때문에 30 중반의 나이로 요절하여 세상을 짧고 굵게 살다 갔다.

그녀는 남들이 도무지 이해를 못 하던 찰스 배비지의 해석 기관의 원리를 간파하고 그 기계의 무한한 가능성을 알아차린 당대의 극소수 덕후 중 하나였다. 오늘날 절차형 프로그래밍 언어의 기본 골격이라 할 수 있는 루프, 조건문, 서브루틴 같은 개념을 떠올렸다. 그것을 처리하는 기계를 만들고 그 틀을 바탕으로 프로그램을 짜서 돌리면, 기계로 음악도 작곡하고 그림도 그리게 할 수 있다고 상상했다. 무려 19세기에 말이다!

배비지 역시 에이다의 재능과 글빨에 큰 감명을 받았다고 한다. 그녀는 기계가 숫자를 처리하기 위해서는 10진법이 아닌 2진법이 유리하다는 발상을 했으며, 베르누이의 수를 구하는 ‘프로그램’을 해석 기계를 기반으로 실제로 작성하기도 했다. 그래서 최초의 프로그래머이다. ㅎㄷㄷ;;; (베르누이는 유체 역학에서 배우는 베르누이의 원리를 발견한 그 과학자 겸 수학자이다. H2O가 뭔지 정도야 '문과 출신'도 알겠지만, 베르누이의 수가 뭔지는 어지간한 이과 출신도 들어 보지 못했을 것이다.)
http://www.bernoulli.org/

그로부터 100년 남짓한 시간이 지나고 전자식 컴퓨터가 실제로 발명되었을 때, 한 절차형 프로그래밍 언어는 바로 이 여성 프로그래머를 기려, 그녀의 이름을 따서 Ada라고 명명되었다. 프로그래밍 언어의 이름에다가 전설적인 프로그래머의 이름을 붙였으니 매우 고무적인 현상이라 하지 않을 수 없다.

Ada 언어는 1983년에 첫 제정되었으며, 이는 시기적으로 C++의 탄생 시기와 일치한다. C++의 고안자가 스웨덴 사람인 반면, Ada의 초창기 핵심 고안자는 프랑스 사람이었다. Ada는 당시 난립하던 프로그래밍 언어들의 통합을 목적으로 미국 국방성으로부터 강력한 후원을 받으며 만들어졌다. 그래서 그쪽 바닥--무기를 가동하는 전산 시스템 같은--에서는 지금까지도 표준 언어로 쓰이고 있다고 한다.

본인은 에이다 언어에 대해서 잘은 모르지만, 이 언어의 문법은 일단 파스칼과 얼추 비슷하다. 암호스러운 기호들보다는, 타이핑을 더 하더라도 깔끔하게 영어 단어 표기를 선호한다. 패키지 단위로 빌드가 이루어지는 것도 C/C++보다는 파스칼 방식이다. (참고로 베이직 언어의 경우, MS의 퀵베이직은 include에 컴파일/링크까지 C언어의 빌드 모델을 그대로 이어받은 반면, 파워베이직은 파스칼과 비슷한 방식을 쓰고 있다는 것이 흥미로움.)

Ada에 대해서도 예제 코드를 보면 이해가 빠를 것이다. 미리 말하지만 C/C++의 사고방식보다는 파스칼의 관점에서 보면 굉장한 동질감을 느낄 수 있다. 심지어 대소문자 구분이 없는 언어라는 것까지도 똑같다. 비록 요즘 C++의 영향을 받은 자바나 C# 같은 언어들의 추세는 대소문자 구분이지만 말이다. 더 자세한 것은 http://www.adahome.com/ 참고.
아니, 그러고 보니 Ada는 수학자의 이름을 따서 지어진 것까지 파스칼과 일치하는구나.

Ada는 기존 언어들의 통합을 목적으로 만들어진 만큼, 이것저것 여러 언어 요소들을 집어넣느라 표현의 자유가 굉장히 넓으며, 제공되는 언어 요소가 많다.
무슨 말이냐 하면, 가령 다차원 배열(a[2,5])과 배열의 배열(a[2][5])을 구분하여 모두 표현할 수 있고,
단축연산이 지원되는 논리 연산자와(and also나 or else), 그렇지 않은 연산자(그냥 and나 or)가 모두 제공된다.

파스칼처럼 0..100 같은 식의 subrange도 당연히 지원되고, 똑같은 정수형이라도 int 같은 기본 자료형과 완전히 호환되는 단순 alias/typedef인지, 아니면 int와 호환되지 않는 별개의 타입인지도 지정 가능하다. 타입 하나는 C/C++과는 비교도 할 수 없이 정교하고 엄격하게 만들 수 있어서 좋다.
함수 안에다 함수도 당연히 만들 수 있고, while이나 for 같은 loop 자체에다가 label 이름을 붙여서 다중 loop을 goto문 없이 바로 탈출하는 것이 가능하다.

이런 것들만 있는 것도 아니고, 어쨌든 Ada는 처음 발표되었을 때는 문법이 필요 이상으로 너무 복잡하고 컴파일러로 다 구현하는 데 난감하다는 불만도 있었다고 한다. Quicksort의 고안자께서도 그렇게 불만을 제기한 사람 중 하나였다고 함. 하지만 오늘날은 C++도 가히 복잡성 면에서는 타의 추종을 불허하는 경지에 도달한 것 역시 사실이다.

그다지 여성향을 느낄 수 없는 것 같은 전산학 분야에도 이런 사연이 있는 여성의 이름을 딴 프로그래밍 언어가 존재한다는 것이 흥미롭다. 여성 프로그래머 모에~ 이다. 하하. =_=;;
참고로 성경에는 Ada와 가장 비슷한 이름으로 유대 왕국의 왕 Asa가 있으나, 물론 성별부터가 다르다.

에이다가 살아 있던 19세기에 우리나라는 뭘 하고 있었는가? 딱 흥선대원군 시절이다. 그런데 본인은 그 시절 조선의 역사에 대해서 좋은 기억이 도무지 없다. 19세기 하면 홍 경래의 난, 전 봉준 동학 운동, 게다가 명성황후 시해 등... 나라는 점점 탐관오리 부정부패와 외세의 침략에 휘말려 막장으로 치닫다가 이내 일제에게 주권을 빼앗기고 만다. 그런데 그 시절에 영국은 저런 상상을 초월하는 학문적 성과가 속속들이 발표되고 있었으니... 오옷 역시 킹 제임스 성경과 철도를 만들어 낸 나라! 괜히 전세계를 호령한 선진국이 아니다.

게다가 전자기학의 대부인 마이클 패러데이(1791-1867), 제임스 맥스웰(1831-1879), 그리고 나중엔 찰스 다윈(1809-1882)이 전부 동시대를 살았던 영국의 과학자들이다! 이 정도면 충격과 공포가 아닐까? 맥스웰도 마찬가지이지만 패러데이는 다윈의 진화론을 단호하게 반박하고 부정한 것으로 유명한 사람인데( http://www.kacr.or.kr/library/itemview.asp?no=644&orderby_1=subject 참고), 에이다로부터 연애편지를 받고서 사랑의 힘으로 더욱 분발(?)하여 전자기력  관련 실험을 성공적으로 마칠 수 있었다 ‘카더라’. 그러고 보니 패러데이는 찰스 배비지와 거의 동갑이니 흠좀무.

끝으로, 잘 알려지지 않은 사실인데, 에이다 부인은 암 치료 과정에서, 당시 통용되던 bleeding (또는 bloodletting) 시술 중에 사망했다. 쉽게 말해 피를 빼내는 작업이다. 옛날 사람들은 병에 걸리면 환자의 혈액에 독소가 차기 때문에 그걸 제거하면 병이 나으리라고 믿었던 모양이다. 마치 체했을 때 손가락 끝을 따는 것처럼? 그런데 그렇게 바늘로 찔러서 몇 방울 따는 것과는 비교도 못 할 정도로 피를 많이 퍼내다가 오히려 환자를 탈수와 쇼크로 인해 죽게 한 경우도 많았다고.

미국의 초대 대통령이며 세계 최초로 자기 임기만 마치고 권좌에서 물러난 지도자인 조지 워싱턴도 bleeding 중에 죽었다. 문헌을 찾아보니 그는 은퇴 후 노후로 인한 폐렴· 천식· 독감 합병증에 걸렸는데, 의사가 치료랍시고 허약한 노인의 피를 5 pint... 거의 2리터가 넘게 빼냈던 것이다. 성인 인체의 전체 혈액이 5리터 남짓이라 하며 헌혈만 해도 아주 건장한 성인 남자를 대상으로 많이 채혈할 때가 3~400ml 정도 하는데, 그의 5배가 넘는 양을 뽑아냈으니 환자의 명을 재촉하는 행위밖에 더 되었겠는가? 그 당시는 혈액에 면역 시스템이 있다는 걸 모르던 시절이었고, 육체의 생명이 피에 있다는 걸(레 17:11) 실감을 못 했었다.

http://gwpapers.virginia.edu/articles/wallenborn.html
http://www.av1611.org/amazing.html

Posted by 사무엘

2010/07/29 08:36 2010/07/29 08:36
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/333

C 언어는 다른 언어가 언어 차원에서 기본으로 제공해 주는 상식적인 기능이 없고, 대신 별도의 함수 호출에 의존하는 형태인 게 몇 가지 있다. 거듭제곱 연산이 대표적인 예이고, 문자열 타입도 언어가 자체 제공하지 않는다. 사실은 동적(힙) 메모리를 할당하는 기능 자체가 아예 없다.

그 이유는 간단하다. 저런 기능들은 컴퓨터 CPU 명령 차원에서 직관적으로 구현 가능하지 않기 때문이다. 그래서 연산자가 그렇게도 많다는 C 언어는 거듭제곱 연산자가 없으며 pow라는 함수를 호출해야 한다. (그나마 파스칼은 그런 함수조차도 없기 때문에, exp와 log 함수 조합으로 임의의 수의 거듭제곱을 얻어내야 한다.)

메모리 할당도 마찬가지이다. 메모리 관리는 CPU뿐만이 아니라 해당 운영체제/플랫폼이 담당하는 비중도 크기 때문에, 작은 언어인 C가 언어 차원에서 자체 제공하지는 않는 것이다. malloc, free, realloc 같은 함수를 써야 한다. 그러면 윈도우 운영체제의 C 라이브러리는 내부적으로 또 HeapCreate, HeapAlloc 같은 더 저수준의 윈도우 API를 이용해서 그런 메모리 관리 기능을 구현해 준다.

그런데 C++에서는 드디어 동적 메모리 할당과 해제 기능이 언어 차원에서 연산자로 추가되었다. 바로 new와 delete 연산자이다. 그때까지 영단어로 이루어진 연산자는 sizeof가 고작이던 것이 새로 추가되었으며, 그 후로 *_cast라든가 typeid 등 여러 영단어 연산자가 C++에 추가되었다. 메모리 할당이라면 몰라도 개체의 생성과 소멸에 따른 생성자와 소멸자 함수 호출은 언어 차원에서 책임져 줘야 하는 영역이기 때문에 별도의 연산자가 생긴 것이다.

연산자가 추가된 덕분에 일단 type casting이나 sizeof 계산을 할 필요가 없게 된 것은 좋다.

pData = new DATA[nCount];
pData = (DATA *)malloc(sizeof(DATA)*nCount);

물론 번거로운 문법 정도야 C 시절에도 매크로로 대체 가능했겠지만 말이다.

#define NEW_C(T, N)  (T *)malloc(sizeof(T)*(N))

그러나 new 연산자는 malloc 함수처럼 범용적인 void* 포인터를 되돌리는 건 지원하지 않으며, 해당 타입의 배수가 아닌 크기의 메모리도 할당할 수 없다. 그렇기 때문에 가변 길이 구조체 같은 메모리를 할당하는 건 오히려 더 불편할 수 있다.
또한 할당 아니면 해제만 지원되지 C 함수처럼 realloc 기능도 없다. C++의 메모리 연산자는 오로지 개체의 생성과 소멸에만 초점을 둔 것이다. 그렇기 때문에 이것이 기존 C의 메모리 관리 함수를 완전히 대체하지는 못할 것으로 보인다.

new 연산자로 데이터 타입을 지정한 뒤에는 new DATA[100] 처럼 배열 첨자가 올 수 있고, 아니면 new Object(x, y)처럼 해당 개체의 생성자 함수에다 넘겨 줄 인자가 올 수도 있다. 두 문법 중 오로지 하나만 허용된다.
그러므로 생성될 때 생성자 함수 인자 전달이 필요한 개체는 배열로 만들 수 없다. 그러나 인자가 필요한 생성자 함수가 존재한다 할지라도, 전부 default argument가 있어서 대체가 가능하다면 배열을 만들 수 있다.

1. new operator vs operator new

이 new 연산자(new operator)는 내부적으로 operator new라는 함수를 호출하는 형태로 구현되어 있으며, 이 특수한 함수는 나름 오버로딩이 가능하다! (delete도 마찬가지) 비록 개체를 생성하여 생성자 함수를 호출한다는 기본 기능은 C++의 특성상 불변이지만, 이 연산자가 하는 일 중 메모리를 할당하고 해제하는 계층은 customize가 된다는 뜻이다.

void *operator new(size_t size);
void operator delete(void *ptr);

operator new 함수는 첫째 인자는 무조건 포인터 크기와 같은 부호 없는 정수형이어야 한다. 부호 있는 정수형도 허용되지 않는다. 그리고 리턴값은 void *이어야 한다.
한편 delete 함수는 첫째 인자는 무조건 void *이어야 하고, 함수의 리턴값은 void여야 한다. 일단 기본적인 생김새는 malloc, free와 완전히 일치한다는 뜻.

당연한 말이지만 이 함수만 단독 호출이 가능하다.
malloc(100)을 쓸 곳에 그냥 operator new(100) 이라고만 써도 된다. 그러면 어차피 new char[100]과 비슷한 효과가 나게 된다. C++ 언어는 이 함수들의 기본 구현을 라이브러리 차원에서 제공하고 있다. 만약 기본 C/C++ 라이브러리를 사용하지 않으면서 new/delete 연산자도 쓰고 싶다면 내가 직접 이들 함수를 구현해 줘야 한다.

거기에다 나만의 인자를 추가한 operator new/delete를 만들 수 있다. 예를 들어, C/C++ 라이브러리가 사용하는 프로세스 기본 힙이 아닌 다른 곳에다가 메모리를 할당하고 싶다면 이렇게 코드를 써 주면 된다.

void *operator new(size_t size, HANDLE hHeap)
{ return HeapAlloc(hHeap, 0, size); }

HANDLE hMyHeap = HeapCreate( ... );
Object *pt = new(hMyHeap) Object( ... );

new 바로 옆에다가 전달해 주는 인자는 operator new의 둘째 이후의 인자로 전달된다. delete도 비슷한 방식으로 오버로딩 가능하다. 놀랍지 않은지?

모든 개체과 기본 자료형에서 통용되는 global scope의 operator new/delete가 있는 반면, 특정 클래스에서만 통용되는 new/delete 함수를 만들 수도 있다. 함수 프로토타입은 동일하다. 이 new/delete 함수는 굳이 static을 지정해 주지 않더라도 언제나 static으로만 선언되기 때문에, 클래스 내부에 있더라도 가상 함수 지정이나 this 포인터는 지원되지 않는다. 또한 생성자· 소멸자· 대입 연산자 등과는 달리, 파생 클래스로 상속도 된다.

2. new operator vs new[] operator

그런데, 더욱 충공깽한 사실은 new와 new[] (delete도 delete[])가 구분되어 있다는 것. 이런 구분이 언제 필요하냐 하면 소멸자 함수가 존재하는 개체의 배열을 선언할 때이다. (물론 기본 자료형이 아니라 개체를 배열로 만드는 경우는 드물지만 말이다.)
우리가 요청하는 메모리의 크기와 실제로 운영체제로부터 할당되는 메모리의 크기는 여러 가지 요인으로 인해 일치하지 않는 경우가 있으며 후자가 전자보다 대체로 더 크게 잡힌다.

배열을 delete로 해제할 때는 여기에 있던 배열 각 원소들에 대해서도 소멸자 함수를 일일이 호출해 줘야 하는데, 원래 여기에 개체가 정확하게 몇 개 있었는지를 메모리 블록만 봐서는 알 수 없게 되는 것이다.
그래서 1980년대에 C++이 처음 등장했을 때는 delete 연산자에다가 배열의 개수까지 지정을 해 줘야 했다.

int *arr = new int[nCount];
Object *ptr = new Object[nCount];
(....)
delete arr; //기본 자료형은 그냥 이렇게 지워도 무방
delete[nCount] ptr; //이놈은 흠좀무

C++은 그렇잖아도 garbage collector도 없어서 불편해 죽겠는데 배열의 원소 개수까지 프로그래머가 관리해야 한다니, 이게 말이나 되는 소리인가?

프로그래머의 원성이 빗발친 덕분에 시스템이 바뀌었다. 배열의 원소 개수는 C++이 메모리를 할당하면서 내부적으로 알아서 관리하도록 바뀌고 원소 개수를 생략 가능해졌다. 그러나 그래도 이게 배열이라는 힌트는 알아서 줘야 한다. 배열일 때와 그렇지 않을 때 C++이 메모리를 관리하고 인식하는 방식은 여전히 서로 약간 다르기 때문이다.

delete arr; delete[] ptr; 를 해도 괜찮다는 소리이지 delete arr; delete ptr; 처럼 구분이 완전히 사라진 건 아니다.

그래서 operator new/delete를 오버로드했다면 operator new[]/delete[]의 오버로드도 지원된다. 둘은 인자의 의미나 하는 일의 차이는 전혀 없다. 단지 new[]의 경우, 연산자의 리턴값 포인터에다가 곧바로 개체가 저장되지는 않는다는 차이가 존재할 뿐이다. 배열 원소 개수가 앞부분에 먼저 저장되고 그 뒤의 공간부터가 쓰인다.

자바와 C#에서 볼 수 있듯, 요즘 대세는 개체는 무조건 new로 선언하는 것이다. 그게 언어 문법까지 더 명료하게 만들어 주는 효과까지 있다. 그러나 C++은 기본 자료형이든 개체든 스택과 힙에 모두 선언 가능하고, 심지어 함수 전달도 둘 다 call by name이나 reference 방식이 모두 가능하다.

일반적으로 컴파일러들은 C++의 operator new/delete도 내부적으로는 C의 malloc/free로 구현한다. 기능이 완전히 동일한데 둘의 동작 방식이 달라야 할 이유가 전혀 없기 때문이다. 그러나 원칙대로라면 malloc으로 할당한 포인터를 delete로 해제한다거나, new로 할당한 메모리를 free로 해제하는 것은 허용되지 않는 비추 행동이다. 그렇게 섞어 쓰지는 않는 게 좋겠다.

Posted by 사무엘

2010/07/28 08:27 2010/07/28 08:27
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/332

C++에는 namespace라는 엄청난 키워드가 존재한다.
namespace는 소스 코드에 존재하는 수많은 명칭(심볼)들로 하여금 이들이 통용되는 구획을 강제로 구분해 준다. (명칭의 decoration도 달라지기 때문에, 링크 때도 동명의 심볼들이 서로 구분 가능함)
방대한 프로그램을 짜고 특히 남이 만든 여러 라이브러리들을 한데 뭉뚱그려 관리하다 보면 함수나 전역 변수 이름, 심지어 매크로 같은 게 겹쳐서 링크 시 충돌이 있을 수 있다. 이때 namespace는 그런 문제에 대한 근본적인 해결책이 되어 준다.

C++은 C에 비해 scope이라는 개념이 더욱 발달했다.
여기서 말하는 scope이란, 단순히 전역 변수냐 지역 변수냐 하는 생명 주기 차원이 아니라, 어떤 심볼이 언어의 문맥 차원에서 인식되고 접근이 허용되는 범위를 일컫는다.
가령, C++ 클래스 내부에 있는 static 변수는 생명 주기로 말하자면야 C의 전역 변수와 다를 바가 없다. 그러나 단순 전역 변수와는 확연하게 다른 scope을 지니고 있다. 그래서 :: 같은 연산자도 생겼다.

예전에는, 특히 C 시절에는 global이라는 기본 namespace 하나밖에 없는 것과 마찬가지였지만 C++에서는 나만의 namespace를 정의할 수 있고, 심지어 이중 삼중으로 namespace 안에 또 namespace를 만들 수도 있다. 심볼들의 입체적인 관리와 구별이 가능해진 것이다.
사실 namespace는 90년대에 나중에 추가된 키워드로, 도스 시절의 볼랜드 C++ 같은 컴파일러에서는 지원도 되지 않는다. (MFC 역시 namespace는커녕 템플릿조차 없던 시절부터 만들어져 온 클래스 라이브러리인지라, namespace를 사용한 흔적이 없음)

그런데, namespace가 하는 일은 클래스가 하는 일과 좀 중복이 있어 보인다.
클래스도 그 자체가 이미 자신만의 새로운 scope을 만들어 낸 것이기 때문이다.
클래스 내부에 public으로 선언된 static 변수 내지 함수하고,
namespace 내부에 존재하는 전역 변수 내지 함수는 언뜻 보기에 위상이 완전히 똑같다.

밖에서는 클래스::이름, 또는 namespace::이름 이렇게 ::을 써서 호칭하는 것마저 동일하다.
클래스도 안에 클래스 내지 구조체가 중첩해서 존재할 수 있으며, 심지어 클래스 내부에서만 통용되는 enum이나 typedef를 선언하는 것도 가능하다.
그럼 도대체 namespace만의 특징은 무엇이 있을까? 아래의 코드를 생각해 보자.

namespace NS {
   class A {};
   void f( A *&, int ) {}
}

//void f(NS::A *&, int) {} //이게 뭘까?

class CS {
public:
   class A {};
   static void g( A *&, int ) {}
};

이렇게만 보면 NS라는 namespace에 소속된 클래스 A와 전역 함수 f,
그리고 CS라는 클래스에 소속된 클래스 A와 전역 함수 g는 서로 그게 그거 같고 정말 차이를 발견할 수 없어 보인다.
다만, class나 struct와는 달리 namespace는 뭔가 인스턴스화하는 자료형을 만드는 것이 아니기 때문에, 닫는 중괄호 뒤에 세미콜론을 붙일 필요가 없다. 뭐, 그 정도 차이는 존재한다.

이들 각 심볼을 외부에서 접근하는 방법도 완전히 동일하다. 아래 코드를 보라.

NS::A *pfm = NULL;
NS::f(pfm, 0); //하지만 바로 f(pfm, 0)만 해도 된다. 이유는 나중에 설명

CS::A *qfm = NULL;
CS::g(qfm, 0);

그런데, namespace는 클래스에 없는 부가 기능이 좀 있다.

첫째, 바로 ADL(Argument dependent name lookup)이라는 기법이다.
C++ 컴파일러는 함수의 argument의 타입으로부터 함수의 소속 scope를 자동 추론하는 기능이 있다.
namespace NS에 속해 있는 f를 호출할 때 굳이 NS::를 할 필요가 없다.
왜냐하면 f가 받는 함수 인자 중에 이미 NS에 소속된 자료형이 존재하기 때문에, 컴파일러는 이 f를 먼저 global scope에서 살펴봐서 없으면 NS namespace 안에서도 찾아보게 된다.

함수의 인자를 이용하여 함수를 추정한다는 점에서는 함수 오버로딩의 확장판이라고 볼 수도 있겠다.
사실, 위의 소스에서 주석을 쳐 놓은 global scope의 f 함수까지 정의한다면 컴파일러는 어느 f 함수를 선택해야 할지 모호하다면서 에러를 낸다.
이런 기능은 클래스에는 존재하지 않는다. g 함수를 호출할 때는 매번 CS::g를 해 줘야 한다.

둘째, using 키워드이다.
반복되는 타이핑을 좀 줄이고 싶어하는 건 프로그래머들의 공통된 희망 사항이다.
타입 선언을 좀더 간편하게 하기 위해서 C/C++에는 typedef라는 키워드가 있고, 베이직이나 파스칼에는 구조체 참조를 좀더 간편하게 하려고 With 같은 키워드가 있다.

그와 마찬가지로 C++에는 여타 namespace에 있는 명칭을 매번 :: 연산자 없이도 바로 참조 가능하도록 using namespace 선언을 제공한다. using namespace std; 처럼 말이다.
using namespace NS를 한번 해 주면, 그 뒤부터는 NS::A *pfm 마저도 A *pfm로 축약 가능해진다.
using의 용법으로는 또 다른 것도 있는데, 설명서를 읽어 봐도 잘 모르겠다. 정말 무진장 복잡하고 저런 걸 언제 어디서 써먹으면 될지 영 감이 안 잡힌다. =_=;;
다만, namespace가 아니라 클래스에 의해 만들어진 scope에 대해서는 그런 것 역시 지원되지 않는다.

셋째, namespace p = FS; 처럼, namespace에다 별명(alias)을 붙여 쓰는 것도 가능하다. 길고 복잡한 다단계 namespace를 손쉽게 축약하는 방법이다. 저런 문법도 있다니, 가히 충격과 공포.

끝으로, 이름 없는 namespace는 마치 C 시절의 static 전역변수/함수처럼, 해당 번역 단위(소스 코드; translation unit) 바깥으로 함수나 변수 심볼이 노출되지 않게 하는 역할을 한다는 것도 알아 두면 좋다.
이 정도 되면 namespace는 C++ 언어에서는 단순히 클래스 이상으로 자신만의 역할이 있다고도 볼 수 있겠다.

가장 먼저 언급한 ADL에 대해서는 비판은 있다. namespace에다가 일종의 예외 규정을 만드는 것이나 마찬가지이기 때문에 C++ 문법을 더욱 복잡하게 하고 컴파일러 만들기도 난해한 언어로-_- 만드는 데 일조했기 때문이다. 그러나 프로그래밍의 편의를 위해서 ADL은 어쩔 수 없이 꼭 필요하기도 하다. 이게 없으면 다른 namespace에 소속되어 있는 클래스의 오트젝트에 대해서는 연산자 오버로딩조차도 제대로 못 하게 되는 경우가 생길 수도 있기 때문이다.

참고로 자바나 C#처럼 C++보다 나중에 등장한 본격 객체 지향 언어들은 C++처럼 global scope이라는 게 존재하지 않는다. 전역 함수나 전역 변수라는 게 애초부터 존재하지 않으며 모든 심볼들은 무조건 클래스에다 소속되어 있어야 한다. 또한 이런 언어들은 C++ 같은 텍스트 include라든가 링크라는 개념이 없으며, 클래스가 곧 패키지요 namespace의 형태로 구조가 잘 짜여 있다. 그래서 C++처럼 namespace를 별도로 갖고 있지는 않다.

Posted by 사무엘

2010/07/07 08:44 2010/07/07 08:44
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/313

C++의 typename 키워드

C++ typename 키워드의 용법은 크게 두 가지이다. 그러나 주된 목적은 동일하다. 다음에 나오는 명칭이 변수도 될 수 있고 변수의 타입 이름이 될 수도 있는 문맥일 때, 이것이 명백하게 후자임을 알려 주는 것이다.

먼저, typename은 잘 알다시피 템플릿 인자를 선언할 때 class 대신 쓸 수 있다.

template<class T> void Swap(T& a, T&b );
template<typename T> void Swap(T& a, T&b );

위의 두 줄은 의미상 완전히 동일하다.
템플릿이 C++ 언어에 처음으로 추가되었던 당시에는 typename이라는 키워드가 없었고 템플릿 인자를 선언할 때에도 class를 썼던 것이다.
그러나 이것은 의미상으로 문제가 있었다. 아래의 예를 보자.

template <class T, int N>
class MyClass {
public:
    T data[N];
};

MyClass<int, 20> obj;

템플릿 인자로는 잘 알다시피 자료형 이름 내지 정수 숫자가 올 수 있다.
그런데 int N에 해당하는 템플릿의 인자로는 마치 일반 함수의 인자처럼 int 값에 해당하는 20이 쓰였다. 그런데, class T에 해당하는 첫째 인자는 그럼 T라는 클래스에 속하는 개체가 쓰인단 말인가?

전혀 그렇지 않다. 여기서는 진짜로 특정 type에 속하는 20 같은 값이 아니라, type 자체가 인자로 와야 하기 때문이다.
그래서 의미상 완전히 다르다는 걸 표현하기 위해 typename이라는 키워드를 class 대신 사용할 수 있게 되었다. 매우 바람직한 조치이다. class라는 키워드는 이제 진짜로 새로운 클래스를 선언할 때만 쓰도록 하자.

그리고 다음 용법이 개념상으로 진짜 중요하다. scope resolution과 관계가 있다.
A가 클래스 이름일 때 A::B라는 표현을 썼다면, C++의 특성상 B는 A의 멤버 변수일 수도 있고, A 클래스 내부에 선언된 다른 타입(클래스, 구조체 따위)의 이름일 수도 있다. 그 클래스 내부에 무엇이 선언돼 있냐에 따라서 해석이 달라진다. 처리가 어렵다.

그런데 설상가상으로 A 자체가 실제로 무슨 타입이 들어올지 모르는 템플릿 클래스의 인자라면? B에 대한 해석은 그야말로 귀에 걸면 귀걸이, 코에 걸면 코걸이가 될 수밖에 없어진다. A의 실체가 무엇이건 B의 정체는 컴파일 시점 때 다 결정되어야 하는데 말이다.

그럴 때 typename A::B를 써 주면, B는 A가 무엇이건 상관없이 변수가 아니라 말 그대로 type 이름으로 처리되어야 함을 컴파일러에게 알려 준다. 이 키워드는 절대적인 모호성 해결보다는, C++의 문법 해석의 복잡성을 좀 줄이고 컴파일러 개발을 더 수월하게 만들려는 목적이 더 크다고 보면 정확하다.
자, 이번에도 이해를 돕기 위해 예제 코드를 보자.

template<typename T>
class MyClass {
public:
    struct MYSTRUCT {
    };
    static MYSTRUCT dat;

    typename T::COMP pp;
};

struct SAMPLE {
    struct COMP {
    };
};

MyClass<SAMPLE> obj;

바로 이런 식으로 MyClass가, 자신의 템플릿 인자 T가 내부적으로 또 갖고 있는 COMP라는 구조체를 이용하기 위해서는 pp를 저렇게 typename을 줘서 선언해야 한다. COMP를 자료형 이름임을 명확하게 해 줄 필요가 있다.

비슷한 이유로 인해,

template<typename T>
typename MyClass<T>::MYSTRUCT MyClass<T>::dat;

템플릿 클래스 내부에 있든 구조체 형태로 된 static 멤버를 밖에서 또 정의해 줄 때, typename을 넣어 줘야 한다.
똑같이 MyClass<T>::로 시작하는 명칭이지만 typename이 선행된 MYSTRUCT는 자료형이고, dat는 멤버 변수로 인식되는 근거가 여기에 있는 것이다.
늘 생각하는 것이지만 C++의 세계는 참으로 심오하다. -_-;;

Posted by 사무엘

2010/06/28 08:56 2010/06/28 08:56
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/305

C++의 템플릿은 두말할 나위도 없이 매우 강력하고 유용한 개념이다.
C++에다가 제네릭/메타 프로그래밍--프로그램을 만드는 프로그램.. 즉 더욱 추상화된 기법--의 가능성을 무한히 열어 줬기 때문이다.

swap, min, max 같은 것부터 시작해서
옛날에는 문법적으로 매우 불완전하기 짝이 없는 매크로를 쓰고 위험한 typecasting을 해야 했던 것을, 템플릿 덕분에 언어의 정식 문법으로 아주 깔끔하고 type-safe하게 구현할 수 있게 된 게 많다.

그리고 static 배열의 크기(원소 개수)를 되돌리는 매크로인 ARRAYSIZE도 생각해 보자.
C 시절에는 sizeof(x)/sizeof(x[0]) 와 같은 식으로 구현했다. 물론 이 값들은 모두 컴파일 시간 때 이미 결정이 되기 때문에 실제로 나눗셈 연산이 일어나지는 않는다.

하지만 템플릿을 이용하면...
template<typename T, size_t N> char (*__GetArraySize(T (&n)[N]))[N];
#define ARRAYSIZE(x)   sizeof(*__GetArraySize(x))

자 이게 무슨 의미인지 이해가 가시는가?
무슨 타입인지는 모르겠지만 어쨌든 N개짜리 배열의 참조자를 받아서 N개짜리 char형 배열의 포인터를 되돌리는 함수를 템플릿으로 선언한다.
그 후, N의 함수의 리턴값을 역참조한 배열의 크기를 sizeof로 구하면... 당연히 char형 배열의 크기는 본디 배열의 원소 개수와 일치할 수밖에 없게 된다. 함수의 몸체를 정의할 필요조차 없다! 마치 &( ((DATA*)NULL)->member ) 가 에러를 일으키지 않고 멤버 오프셋을 되돌려 주는 것과 같은 이치이다.

템플릿이 아예 배열의 참조자를 받게 함으로써 좋은 점은, 이 매크로에다가 static 배열이 아니라 단순 포인터를 집어넣으면 컴파일 에러가 발생하게 된다는 것이다. 단순 sizeof(A)/sizeof(A[0])보다 타입 safety도 보장되고 훨씬 더 좋다.
(), *, [] 등이 뒤섞인 복잡 난해한 C/C++ type string 읽는 법에 대해서는 이전에 별도로 글로 다룬 바 있다.

template<typename T, size_t N> size_t ARRAYSIZE(T (&n)[N]) { return N; }

물론 위와 같이 코드를 쓰면 더 간단하고 알아보기도 쉽게 동일한 효과를 이룰 수 있지만, 최적화를 하지 않은 디버그 빌드는 불필요한 함수 호출이 계속 일어나는 문제가 있다. 배열의 크기 정도는 컴파일 타임 때 모든 계산이 딱 일어나게 하는 방법을 쓰는 게 더 좋을 것이다.

이렇게 템플릿은 매우 편리한 개념이긴 하나, 한계도 분명 있다.
템플릿은 동일한 패턴을 지닌 여러 다른 코드들을 찍어내는 ‘틀’과 같다. 하지만 이 틀 자체는 한 번만 정의해 놓고 링크 타임 때 여러 오브젝트 파일 사이를 자유자재로 넘나들게 할 수는 없다. 최적화처럼 기술적으로 여러 난관이 있기 때문이다. 템플릿 인자로 들어온 타입이 int일 때, double일 때, 심지어 개당 100바이트가 넘는 구조체일 때 이들에 대한 각종 비교나 대입 연산과 최적화 방식과 코드 생성 방식은 완전히 천차만별이 될 수밖에 없다.

이런 이유로 인해 템플릿의 정의 효과는 오로지 한 소스 코드, 한 translation unit 안에서만 유효하며, 템플릿 클래스나 함수는 모든 소스 코드에 헤더 파일의 형태로 매번 include되어야 한다. 몸체까지(body; definition; implementation) 죄다 말이다. 일반적인 클래스의 함수처럼 선언 따로, 정의 따로일 수가 없다. 사실은 템플릿 코드에 대한 에러 체킹 자체도 템플릿이 인자가 들어와서 어떤 형태로든 실현(realize)이 됐을 때에야 할 수 있다.

그러니 템플릿으로 구축된 각종 함수와 클래스는 소스 코드가 노출될 수밖에 없으며, 그 소스 코드를 고치면 템플릿을 include하는 모든 소스 파일들이 재컴파일되어야 하는 등 프로그래밍 상의 한계가 결코 만만한 수준이 아니다. 하지만 이 한계는 C++ 언어의 컴파일/링크 모델이라든가 기존 컴파일러들의 오브젝트 파일 포맷 내지 컴파일러/링커의 동작 방식이 근본적으로 바뀌지 않는 한 극복되기 쉽지 않을 것이다.

이런 구조적인 불편을 해소하고자 C++ 표준 위원회가 제안한 것은 export 키워드이다.
흔히 import/export하면 윈도우 프로그래밍 세상에서는 DLL 심볼을 내놓거나 가져오는 개념을 떠올리는데, 이 문맥에서는 그런 건 아니다. 한 translation unit에 존재하는 템플릿 함수 구현체를 다른 translation unit이 그 경계를 초월하여 링크 타임 때 가져다 쓸 수 있게 하는 흠좀무한 개념이다. 즉, 템플릿 몸체에 대한 export를 뜻한다.

헤더 파일에다가는

export template<typename T> void Swap(T& a, T& b);

이라고 해 놓고 모처의 cpp 파일 한 군데에다가는

export template<typename T> void Swap(T& a, T& b)
{
    T c(a); a=b, b=c;
}

이런 식으로 써 놓음으로써,
Swap 함수는 export라고 마크가 되어 있으니 자기 translation unit에서만 쓰지 말고, 다른 단위에서도 필요하면 링크 때 가져다 쓸 수 있게 한다는 게 당초 의도였던 모양이다. (인터넷 검색을 해 보니)
템플릿으로 들어간 클래스 멤버 함수에 대해서도 마찬가지이다. export가 없으면 저 함수 body는 마치 static 변수/함수처럼 그 소스 파일 내부에서만 유효하고 다른 소스 파일에서는 링크 에러가 나게 될 것이다.

그러나 이것을 본 컴파일러 개발사들은 '이뭐병, 이딴 걸 무슨 얼어죽을 표준안이라고 내놓냐' 하는 반응이었고..
비주얼 C++, gcc 등 유수의 컴파일러들은 이 키워드의 구현을 포기/거부하고 말았다.
비주얼 C++의 경우 도움말에 Nonstandard Behavior로 자기는 이 키워드를 지원하지 않는다고 당당히 명시까지 되어 있다. 2010은 모르겠고 2008까지도 마찬가지임.
하지만 마이너급 컴파일러 중엔 export를 구현 안 한 놈이 없는 건 아니라고 한다. 흠좀무.

컴파일해 놓은 코드를 짜깁기만 하는 게 링크인데, 저걸 구현하려면 링크에다가 다시 컴파일을 하고 재링크(?) 과정을 집어넣어야 한다. C/C++의 정상적인 빌드 루트와는 정면으로 모순되는 과정을 요구하는 것이다. 그래서 컴파일러 개발사들이 떡실신한 것이다.

어쨌든 이런 이유로 인해서 export는 C++의 흑역사 키워드로 전락해 있다.
옛날에 MS는 링크 과정을 최대한 간단하게 만들려고 COFF 방식 obj 파일과 PE 방식 exe 파일을 채택했다고 하던데, 하지만 요즘은 워낙 translation unit을 넘나드는 링크 타임의 코드 생성과 전역 최적화 같은 기술이 대세가 돼 있다 보니, export 키워드의 의도가 옛날만치 그저 병맛나게 들리지만은 않을 것 같은 생각이 들기도 한다.

마치 유명무실하던 auto가 리모델링되고 서울 지하철 5호선 마곡 역이 13년만에 부활했듯이 export 키워드도 의도 자체는 좋은데.. 언젠가 부활할 날이 올 수 있지 않을까? 하지만 C++의 차세대 표준인 C++0x에서는 export를 아예 빼 버리고 백지화하자는 제안까지 나온 상태이니, 과연 지못미이다.

Posted by 사무엘

2010/06/12 09:27 2010/06/12 09:27
, ,
Response
A trackback , 11 Comments
RSS :
http://moogi.new21.org/tc/rss/response/293

C/C++의 type string은 간단한 건 간단하지만 복잡한 건 한없이 복잡하다. C/C++ 프로그래밍 경력 10년이 넘는 본인조차 아직 그런 쪽에는 능숙하지 않으며, 좀 복잡한 type 선언을 해야 하면 옛날에 짜 놓은 코드를 복사해서 가져온다. -_-

복잡한 게 뭔지를 물으신다면, 이런 것을 말한다. 특정 함수의 포인터, 배열의 포인터를 되돌리는 함수의 포인터, 포인터의 참조자, C++ 멤버 포인터 등등... 생각만 해도 머리가 뱅뱅 돌지 않는지?

C/C++에서 뭔가 명칭을 선언하는 건 아래와 같이 일면 단순하다. 간단한 것, 상식적인 것부터 살펴보자.

type p;

이렇게 써 주면 p라는 명칭은 type이라는 타입으로 선언된다. p는 변수가 될 수도 있고 함수도 될 수도 있고 포인터나 배열 변수가 될 수도 있다. C++은 함수 내부의 아무 위치에서나 변수를 선언할 수 있으나, 함수 안에서 또 함수를 선언할 수는 없다. nested 함수라는 개념이 존재하지 않는 것이다.

type a, b, c;

처럼 콤마를 써서 여러 명칭을 동일 type으로 동시에 선언할 수도 있다.
type에는 int, float 같은 built-in type이 들어갈 수 있고, 사용자가 예전에 정의한 구조체· 공용체나 클래스가 들어갈 수도 있다.

C에서는 구조체· 공용체의 명칭 앞에 struct나 union 키워드를 생략할 수 없으며 생략하려면 typedef를 별도로 만들어야 하는 부조리가 있었으나, C++에서는 그런 한계가 없어졌다. type이 템플릿인 경우, 템플릿을 실제로 만들어 내는 argument도 < >에다 둘러싸서 넣어 줘야 하며, 타입 명칭이 다른 scope에 존재할 경우 :: 연산자도 써 줘야 한다. std::vector<int>처럼.

type 명칭에는 이 변수의 성격을 규정하는 modifier 키워드도 선택사항으로 들어갈 수 있다. 이런 예로는 const, volatile, register 같은 키워드가 있다.

type에 대한 설명은 여기까지로 하고, 그럼 p(명칭)에 대해 알아보자.
명칭은 한 번에 여러 개를 동시에 선언할 수 있고, 또 원한다면 p=1처럼 =을 써서 선언과 동시에 초기화도 가능하다. C++의 경우, 아예 ()을 써서 생성자 함수 호출을 바로 시키는 것도 가능하며 built-in type에 대해서도 생성자 함수 호출하듯 값을 초기화할 수 있다. 즉,

int *a=NULL, b=7; /* C style */
int *a(NULL), b(7); //C++ style

C에서는 위의 문장만 허용되는 반면 C++은 아래의 문장도 허용된다는 뜻이다.

자, 그럼 이제 진짜 복잡한 부분으로 들어가 보겠다.
C/C++의 문법이 판타지 같은 이유는, 분명 명칭의 type과 관련된 modifier들이 type 부분에 확실하게 구분되어 있는 게 아니라 name 부분으로 개별 적용되는 것도 있기 때문이다. 그렇기 때문에 C/C++은

int *a, b;

라고 선언하면 *라는 modifier는 a에만 적용되어 a만 int형에 대한 포인터가 되고 b는 일반 int가 되는 것이다. 그런데 D라는 언어는 그렇지 않아서 위와 같이 선언하면 a와 b의 타입이 모두 int*가 된다.

이런 식으로 개별적으로 적용되는 modifier로는 다음과 같은 것이 있다. 이런 것들이 막 섞이면 사람 머리 터지게 만든다. ^^;;

*p : p가 포인터임을 뜻한다. 변수의 왼쪽에 붙으며, 오른쪽에서 왼쪽으로 해석한다. *가 여러 개 붙으면 2중, 3중 포인터가 될 수 있다. (pointer to)
&p : C++에서 추가된 문법이며, p가 참조자임을 뜻한다. 쓰임이 포인터보다 훨씬 제한적이기 때문에 다중으로 붙을 수 없다. 용법은 *와 동일. (reference to)

int *&p;

라고 하면 우에서 좌로 & → * 순으로 해석되어 p는 포인터의 참조자가 된다(a reference to a pointer to integer). 반대로 참조자를 가리키는 포인터라든가 참조자를 또 가리키는 참조자라는 개념은 C++에 없기 때문에, &*나 && 같은 문법은 틀렸다. 포인터의 문법을 간소화하려고 만든 게 참조자인데 이는 상식적으로 당연한 얘기. 하지만 이중 포인터의 참조자인 **&은 있을 수 있다. 이 정도면 *와 &의 관계는 충분히 설명됐을 것이다.
다음,

p() : 어떤 명칭 바로 오른쪽에 ()가 붙었다면 이는 그 명칭이 함수임을 뜻한다. 쉽다.

p[n] : 그 명칭이 배열임을 뜻한다. 첨자가 들어있어야 하는 게 원칙이지만, 함수 argument라든가 일부 1차원적인 문맥에서는 첨자가 생략되어서 포인터와 별 차이 없는 용법이 되기도 한다. 영어로는 array of에 해당. []가 오른쪽 끝에 계속 붙으면 다차원 배열을 만들 수 있다.

그렇다면 명칭의 왼쪽에 포인터가, 오른쪽에 ()나 []가 다 붙어 있으면 어떻게 해석해야 할까?
일단 오른쪽 것부터 해석한다. 그 후 오른쪽 끝에 도달하면 왼쪽으로 간다. 그래서

int *a[10];

은 []이 먼저 해석되어 array of / pointer to / int가 되고, 따라서 ‘int *가 10개 있는 배열’이 된다.
이 순서를 바꾸기 위해서 또 괄호가 사용된다. 함수를 뜻하는 ()와는 쓰이는 문맥이 다르며, 의미도 다르다. 이걸 아는 게 중요하다.

int (*a)[10];

은 *이 먼저 해석된 후 오른쪽의 배열로 넘어가서 pointer to array[10] of int가 되고, 따라서 배열의 포인터가 된다. 사실, C/C++의 type string은 일종의 영어 어순을 따르고 있는 셈이다. 이걸 알면 쉽다. 꼭 기억하자.

int func(int x);
int (*funcptr)(int x) = func;

명칭 다음에 곧바로 ()가 나오면 함수 선언이 되나, 이름이 괄호로 둘러싸여서 *가 먼저 해석되므로 funcptr은 pointer to function, 즉 함수의 포인터가 되고, 자신과 prototype이 완전히 같은 func라는 함수를 가리킬 수 있게 되는 것이다.

닫는 괄호를 만나면 아직 해석되지 않았던 왼쪽으로 이동하고, 그러다가 여는 괄호를 만나면 다시 닫는 괄호 바깥의 오른쪽으로 가면서 완전히 바깥에 도달할 때까지 이 과정을 반복하면 된다.
따라서 명칭 뒤에 붙는 (), *, [] 같은 게 아무리 복잡하더라도, 명칭의 좌우에 가장 가까이 붙어 있는 놈이 뭔지만 보면, 얘가 포인터인지 함수인지 배열인지 정도는 바로 알 수 있다.

double ( *varr( double (*)[3] ) )[3];

위는 배열의 포인터와 함수의 포인터가 모두 동원된 예이다. 슬슬 머리가 아파질 것이다. varr의 좌우로 *와 ()가 있는데, 이때 오른쪽으로 먼저 간다. 그래서 varr은 함수가 되고 왼쪽의 *는 함수의 리턴값과 관계가 있게 된다. 그렇다. 이놈은 double 형 배열의 포인터를 인자로 받는 함수인데, 이 함수의 리턴값 역시 double 형 배열의 포인터라는 뜻이다.

double (* (*pfnFunc)( double (*)[3] ) )[3] = varr;

그리고 저 varr을 가리키는 함수의 포인터는.. varr만 (*pfnFunc)라고 또 감싸 주면 만들 수 있다. ^^;; 포인터를 되돌리는 함수의 포인터인 것이다.

int *(*(*fp1)(int))[10];

굉장히 변태-_-스러운 예제인데, 별표를 맨 왼쪽에 있는 것부터 [1], [2], [3]으로 번호를 매기자면,
fp1은 int 형을 인자로 받고, 원소 개수가 10인 int 포인터[1]의 배열에 대한 포인터[2]를 되돌리는 함수의 포인터[3]이다.

pointer to *
function (int)
returning pointer to *
array [10] of int*

이제 진짜 궁극의 변태 같은 예를 들면,

char *(*(**foo[2][8])())[10];

array [2][8] of
pointer to **
function ()
returning pointer to *
array [10] of char*

다시 말해 char*가 10개 들어있는 배열의 포인터를 되돌리는 함수의 2중 포인터를 담고 있는 2차원 배열이라는 소리이다. ^^;;

그럼 마지막으로, 또 하나의 기괴한 C++ 문법을 소개하면서 글을 맺겠다. 그것은 바로 멤버 포인터라는 특이한 포인터이다.

class CMyObject {
public:
 int x,y,z;
 void foo() {}
 void bar() {}
};

CMyObject obj;
int CMyObject::*pVal = &CMyObject::x;
void (CMyObject::*pFunc)() = &CMyObject::foo;

obj.*pVal = 10;
(obj.*pFunc)();

위의 코드에서 볼 수 있듯 pVal은 int형인 x, y, z중 한 멤버 변수를 가리킬 수 있고, pFunc는 자신과 prototype이 같은 foo()와 bar() 중 하나를 가리킬 수 있다.
일반적인 C++ 클래스의 non-static 멤버들은 멤버 포인터로 하여금 자신을 가리키게 할 때 "&클래스::멤버"와 같은 식으로 주소를 얻을 수 있다. 이때 어느 토큰 하나도 생략할 수 없다. 심지어 자기 클래스 멤버 함수 내부에서라도 자기 클래스 이름을 반드시 명시해야 한다.

멤버 포인터를 나타내는 ::*은 ::와 *가 합쳐진 것이다. 그러나 멤버 포인터를 실제로 사용하는 연산자인 .* 또는 ->* 는 완전히 한 토큰으로, 사이를 띄울 수 없다. 또한 멤버 포인터 함수를 선언하고 호출할 때는 반드시 괄호가 필요하다. 이걸 하지 않으면 오른쪽의 함수 호출 ()가 먼저 해석되어서 개체와 멤버 포인터가 먼저 연결되지 못하기 때문이라 한다.
마치 파스칼 언어에서 우선순위 처리의 특이점 때문에 (a=1) and (b>5)처럼 각 항을 괄호로 싸 줘야 하는 것과 비슷한 맥락이라 하겠다.

그나저나 C++은 :: . -> 이렇게 세 연산자가 모두 따로 존재하는 언어라는 게 특이하다. 자바나 C#은 . 하나가 이들 기능을 모두 수행한다.

Posted by 사무엘

2010/05/29 15:20 2010/05/29 15:20
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/279

C++에서 A라는 클래스를 만들었다. 이 클래스는 앞으로 당신이 만들 거의 모든 클래스들이 상속 받을 아주 기본적이고 공통적인 기능을 갖추고 있다. COM으로 치면 IUnknown, MFC로 치면 CObject 같은 기능을 하는데, 여기서는 그 예로 자체적인 reference counting 기능을 내장하고 있다고 치자.

class A {
 int nRefCnt;
public:
 A(): nRefCnt(1) {}
 ~A() {}
 int AddRef() { return ++nRefCnt; }
 int Release() { int nt=--nRefCnt; if(nt==0) delete this; return nt; }
};

이제 당신은 A로부터 상속 받은 여러 클래스들을 만든다.

class B: public A {
public:
 int nAddVal;
 B(): nAddVal(2) {}
};

class C: public A {
public:
 int nExitVal;
 C(): nExitVal(3) {}
};

그런데 C++에는 다중 상속이라는 게 존재한다.
어쩌다 보니, A의 자식 클래스들 중 서로 다른 클래스를 골라서 이들의 기능을 다 물려받은 클래스를 만들고 싶어진다. (욕심도 참 많다!)

class D: public B, public  C {
public:
 int nLast;
 D(): nLast(4) {}
};

여기서 문제가 생긴다.
이 경우, D는 B와 C의 기능을 물려받는 과정에서, B와 C가 공동으로 소유하는 A는 두 번 상속받게 된다.
32비트 기준으로 obj의 멤버 배열 순서는 대략 "1, 2, 1, 3, 4" 정도가 된다.

D obj;
obj.AddRef();

이 코드의 실행 결과는 어떻게 될까?
고민할 필요 없다. 이 코드는 컴파일 자체가 되지 않을 테니 말이다. =_=
D라는 클래스에는 A의 nRefCnt라는 멤버 자체가 둘 존재한다.
그렇기 때문에 B 쪽에 속하는 nRefCnt를 건드릴지, C 쪽에 속하는 nRefCnt를 건드릴지 판단할 수 없어서 컴파일러는 모호성 오류를 일으키는 것이다.
클래스 가계도는 보통 tree 구조가 되는데 이 경우 엄밀히 말하면 cycle이 존재하게 된다. 이 cycle을 일명 '죽음의 다이아몬드'라고 부른다.

obj.B::AddRef() 내지 obj.C::AddRef()라고 구문을 바꾸면 컴파일 에러 자체는 없앨 수 있다.
그러나 이것은 미봉책일 뿐이지 문제를 본질적으로 해결하는 방법은 아닐 것이다.
이건 클래스 B와 C가 아무 공통분모 없이, 우연히 AddRef라는 껍데기만 동일하고 의미는 완전히 다른 함수를 제각기 갖고 있는 것과 다를 게 없는 상황이다.
근본적으로는 D라는 클래스는 비록 B와 C의 기능을 동시에 상속 받았더라도 A는 단 한 번만 상속 받게 하는 방법이 있어야 한다. 그게 가능할까?

그래서 C++은 '가상 상속'이라는 걸 제공한다.
일반적으로 B라는 클래스가 A라는 클래스로부터 상속을 받았다면, B라는 클래스는 내부적으로 A의 몸체 뒤에 자기 몸체가 덧붙는다. 따라서 B 클래스의 오프셋과 A 클래스의 오프셋 사이의 간격은 컴파일 시간 때 딱 결정이 되어 버리며 언제나 고정 불변이다.

그런데 B가 A를 상속 받으면서 A를 '가상'으로 상속하면, B 클래스로부터 A 클래스의 오프셋은 자기가 별도의 내부 멤버로 갖고 있게 되며, 컴파일 시점이 아니라 실행 시점 때 동적으로 바뀔 수 있게 된다. 기반 클래스가 특수한 처리를 하는 게 아니라, 상속을 받고 싶어하는 자식 클래스가 상속을 특수한 방법으로 받아야 한다.
그래서 위의 네 클래스 A~D 중, 죽음의 다이아몬드를 해소하기 위해서는 B와 C가 A를 virtual로 상속 받게 하면 된다.

class B: virtual public A { ... };
class C: virtual public A { ... };

이 경우 B와 C는, 기반 클래스인 A가 자신과 메모리 상으로 굳이 연속적으로 이어져 있지 않더라도, B나 C 자신이 스스로 갖고 있는 부가 정보를 통해 기반 클래스인 A의 위치를 추적할 수 있다.

클래스 C만 갖고 생각하는 경우, 당연히 메모리 상으로는 A와 C가 바로 따를 것이고, C 내부에 있는 A의 포인터는 자기 바로 앞을 가리키고 있을 것이다.
하지만 클래스 D는 멤버가 ABCD와 같은 순으로 쫙 배열될 수 있으며, A와 C 사이에 B 같은 다른 클래스가 있을 수 있다. 그래도 B와 C가 A를 공유할 수 있게 된다. 공유를 하려고 이 지저분한 짓을 자처한 것이다.

자, 그럼 가상 기반 클래스의 구현 비용은 어느 정도 될까? C++에서

ptr->Function(a, b);

이라는 문장이 있고 Function이 virtual이 아닌 일반 클래스 멤버 함수라면, 위의 코드는 C 언어 문법으로 표현했을 때 대략

Function(ptr, a, b);

이 된다. 즉, this만 암묵적으로 추가되고 일반 함수와 완전히 똑같은 형태이다. 가장 간단하다.
하지만 Function이 가상 함수라면,

ptr->functbl->Function(ptr, a, b);

과 같은 꼴이 되고 오버헤드가 꽤 커진다. 우리 멤버가 가리키는 공용 가상 함수 테이블로 가서 거기서 함수 포인터를 참조하는 셈이다.
그렇다면 Function이 ptr이 가상으로 상속 받은 기반 클래스의 비가상 멤버 함수라면,

Function(ptr + ptr->baseptr, a, b);

정도. 가상 함수 정도는 아니지만 그래도 this 포인터의 위치 계산을 위해서 두세 개의 명령 오버헤드가 추가된다.
일단 다중 상속이라는 개념 자체가 컴파일러 문법상의 단순 형변환일 뿐이던 typecasting을 굉장히 복잡하게 만들었다는 점을 알 필요가 있다.

그럼 Function이 가상 상속에다가 가상 함수이기까지 하면 어떻게 될까?
그래도 functbl을 찾는데 오버헤드가 더해지지는 않는다. 어차피 각 클래스의 함수 테이블에는 자기가 지금까지 상속 받은 모든 클래스의 가상 함수들이 누적 기록되어 있기 때문에, 함수 호출을 위해 여러 테이블을 돌아다니지는 않는다.

참고로 A에 가상 함수가 있고 B와 C가 이를 제각기 오버라이드를 했는데 D가 B와 C를 동시에 상속 받고도 그 가상 함수를 또 오버라이드하지 않았다면, 컴파일 에러가 난다. B와 C 중 어느 장단에 맞춰 춤을 추리요? C++ 컴파일러는 그 정도 모호성은 자동으로 지적해 준다. C++ 컴파일러 만들기란 정말 힘들겠다는 생각이 들지 않는가?

가상 함수, 가상 상속, 멤버 함수 포인터... =_=;;
오늘날 프로그래밍 업계에서 다중 상속은 굉장히 지저분하고 흉악한 개념으로 간주되어 금기시되고 있다. C언어의 전처리기, 포인터와 더불어 현대의 프로그래밍 언어에서는 찾을 수 없는 면모가 되고 있다.
여러 클래스의 기능이 한꺼번에 필요하면, 무리하게 상속으로 해결하지 말고 해당 클래스 개체를 '멤버'로 가지는 쪽으로 가라는 것이다.

다중 상속이라든가 가상 상속이 골치아픈 게 결국은 데이터 멤버들의 오프셋 계산 때문이다. 하지만 가상 함수만 잔뜩 만드는 것은 그 클래스 자체가 아닌 함수 테이블의 덩치를 대신 키우는 것이고 클래스 자체는 가상 상속 같은 복잡한 테크닉을 필요로 하지 않기 때문에, 대다수 현대 객체 지향 언어들은 '인터페이스'라는 개념을 도입하여 이것으로 다중 상속의 기능을 어느 정도 대체하고 있다.
사실, C++의 각종 어려운 OOP 개념을 실제로 구현하는 데 비용이 얼마나 드는지를 잘 이해하고 있는 것만으로도 상당한 수준의 프로그래밍 내공을 쌓을 수 있다!

Posted by 사무엘

2010/04/14 17:19 2010/04/14 17:19
,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/245

C는 작고 쪼잔하고 오덕스럽게 만들어진 언어이다(이런 특성을 상당수 물려받은 C++도 포함). 문법에서도 이런 면모가 발견되는데, 가능한 한 예약어 개수를 줄이고 연산자와 기호만으로, 그리고 이 토큰이 쓰인 주변 문맥을 통해서 구문의 의미가 파악되도록 언어를 설계한 것이다.

비슷한 계열의 구조화 프로그래밍 언어인 파스칼과 비교하면 문법이 얼마나 극단적으로 다른지 알 수 있다. begin end 대신 간단히 { } 이다. function, procedure처럼 서브루틴을 나타내는 예약어가 따로 존재하지 않는다. 그냥 자료형과 ()가 함수를 나타내며, 아예 void라는 예약어가 따로 존재한다. var 같은 예약어도 없이 변수 선언이 바로 가능하다. forward 같은 예약어가 없어도 함수의 선두 선언이 가능하며, 별도의 array 예약어가 없이 배열을 선언할 수도 있다. 순수 가상 함수를 선언하는데  pure 같은 별도의 예약어를 추가한 게 아니라 그냥 함수 = 0이란 표현으로 대체한다. 이게 바로 C++의 사고방식이다.

이렇게 극도로 함축적인 문법 덕분에, 프로그래머는 일단 타이핑을 덜 해도 되니 좋다. 1970년대에는 언어도 기계 저수준 프로그래밍을 위해 한없이 쪼잔해져야만 했던 때임을 기억할 필요가 있다. 그 시절에 무슨 인텔리센스라든가 코드 자동 완성 같은 사치스러운 기능이 있었단 말인가?

하지만 이런 언어 구조 때문에 C, 특히 C++은 코드를 알아보고 구문 분석하기가 무척 까다로운 언어가 되고 말았다. 사람에게만 힘든 게 아니라 컴파일러 입장에서도 말이다. 단순히 암호 같은 포인터 참조와 연산자 남발 때문에 알아보기 어려운 차원이 결코 아니다.

전산학적으로 말하면 C/C++의 문법은 문맥 자유 문법이 아니다. 가령 C++ 언어의 global scope에서,

a b(c, d);
위의 문장은 C++의 경우 함수의 선언일까, 아니면 개체의 선언일까?

a<100> b;
그리고 위의 문장은 템플릿을 이용한 개체일까, 아니면 비교 연산일까?

즉, a~d의 타입이 무엇이냐에 따라 구문의 의미, 즉 파싱 방법이 완전 극단적으로 달라진다. 마치 보는 방식에 따라 GOOD으로도 보이고 EVIL로도 읽히는 중의적인 그림처럼 말이다.
C++의 문법은, 의미를 파악하기 위해 파싱을 하는데 각 토큰의 의미를 모르면 제대로 파싱을 할 수 없는 그런 구조인 것이다!

그렇기 때문에 C++ 코드는 IDE 차원에서 간단한 인텔리센스나 자동 완성 기능만 구현하기 위해서라도 코드를 전부 읽어서 사실상 컴파일을 해 봐야 한다. 게다가 전처리기를 거쳐서 #define 심볼까지 일일이 벗기면서 말이다. C#이나 자바는 C++과 매우 유사한 구문을 갖고 있고 똑같이 { } 블록 구조이지만, 문맥 자유 문법을 갖추고 있으며, 의미 분석이 C++보다 훨씬 더 간단하다.

파스칼은? 더 말이 필요 없다. 소스 코드를 단 한 번만 읽으면서 앞으로 되돌아갈 필요조차 없이 구문 분석 + 코드 생성이 다 되는 구조이다! 물론 같은 의미를 표현하더라도 C/C++보다 거추장스럽고 프로그래머가 불편한 게 더 많긴 하지만 말이다.

자바와 C#이 C++에 존재하는 모호성을 없앤 것 중 하나는 new 연산자이다.
생성자 함수 호출을 동반하는 개체는 무조건 new로 선언하게 되어 있기 때문에, new가 동반되지 않은 a b(c, d) 같은 구문은 일단 개체 선언은 절대 아니고 함수 선언이라고 보장할 수 있는 것이다.

C/C++의 문법을 더욱 문맥 의존적이고 지저분한 판타지로 바꾼 것 중 하나는 type casting이다. 별도의 type casting 연산자나 예약어가 존재하는 게 아니라 그냥 앞에다가 타입 이름을 써서 괄호로 싸는 걸로 형변환이 되게 만들어 버렸으니 원...  (a)+b 라는 구문에서 +는 a가 무엇이냐에 따라서 이항 연산자일 수도 있고 아닐 수도 있다. 포인터의 의미를 겸하고 있는 * 까지 가면 더욱 복잡해진다.

게다가 C++에서는 생성자 변환 스타일까지 허용되니 더욱 지저분해졌다! (type)value 뿐만 아니라 type(value)까지 된다는 소리. 이런 어정쩡한 문법 때문에, 소스 코드에서 명시적인 형변환이 일어나는 곳만 딱 찾기도 곤란하다는 점 역시 큰 문제였다.

보다못해 1990년대 중반에는 C++에 4종류의 별도의 형변환 연산자가 예약어로 추가됐다. static_, dynamic_, reinterpret_, const_로 시작하는 cast 연산자가 그것이다. 취지는 좋은데 C언어 철학과는 전혀 어울리지 않을 정도로 예약어 길이가 너무 긴 게 흠이다.

C++은 C언어의 호환성을 존중하여 설계되었지만, 그렇다고 해서 C의 strict superset으로 설계된 것도 아니다. 일부 문법은 바뀌거나 더 엄격해졌기 때문에, 어떤 C 코드는 C++ 언어 문법으로는 컴파일이 되지 않는다. C 영역과 C++ 영역을 엄밀하게 분리한 것도 아니고 그냥 어중간하게 C에다가 OOP 개념을 집어넣다 보니 문법은 더욱 복잡해지고, 특히 동일한 개념을 나타내는 문법이 여럿 존재하는 등( (int)a, int(a)라든가, 포인터와 참조자 중복처럼 ㅋㅋ), 참을 수 없는 지저분함에 환멸을 느끼는 프로그머도 존재할 정도이다.

그래도 오늘날까지 컴퓨터와 직통으로 네이티브 대화가 가능한 가장 강력하고 효율적인 언어라는 장점, 오로지 그거 하나 때문에 C/C++은 메이저급 언어로 군림 중이다. 더 깔끔하고 수학적으로 엄밀한 프로그래밍 언어를 좋아하는 사람이라면 이런 현실을 결코 달갑게 여기지 않겠지만 말이다.

Posted by 사무엘

2010/04/10 19:20 2010/04/10 19:20
,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/241

C/C++ 언어는 언어 차원에서 자체 제공하는 문자열 타입이란 게 없다.
문자열은 단순히 문자의 배열 내지 이를 가리키는 포인터로 취급되며, 코드 번호 0인 문자가 문자열의 끝을 나타내는 매우 원시적이고 단순한 방법을 사용하고 있다. 그 이름도 유명한 null-terminated string 기법이다. (이하 편의상 NTS로)

깔끔하고 쓰기 편한 문자열 처리를 제대로 구현하는 데 드는 비용을 감안한다면, 이것은 그렇게 나쁜 방법은 아니다. string은 int보다야 처리하기가 훨씬 무거운 건 사실이다. 더구나 C/C++은 정말 1바이트라도 더 아끼고 한 클럭이라도 더 줄여야 하는 시스템 프로그래밍 용도로 개발된 언어인 것이다.

NTS 방식으로 문자열을 다루지 않는 언어도 있다. 이런 언어는 문자열의 길이는 별도의 장소에 보관하며 스트링의 내부에 0번 문자가 있을 수도 있다. 그런데 이런 언어로 프로그램을 짜더라도, NTS를 받는 운영체제 API에다 문자열을 넘겨줄 때는 뒤에다 0번 문자를 손수 추가하여 문자열을 변환하곤 한다. 운영체제 커널이야말로 C 언어 기반인 경우가 태반이기 때문이다.

하지만 다른 모든 곳에는 NTS 문자열 포인터만 받더라도, 그래픽 API는 비록 C언어 스타일이라 할지라도 이례적으로 문자열 포인터뿐만 아니라 문자열의 길이를 별도의 인자로 따로 받는 경우가 많다. 윈도우 API도 그 대표적인 예인데, 그 이유는 명백하다. 그래픽 처리의 특성상 NTS 문자열을 일부 몇 글자만 찍어야 하는 경우도 빈번하게 발생하기 때문이다.

NTS는 간단하고 효율적인 대신, 문자열의 끝을 언어나 프레임워크 차원에서 보장을 하지 않는다는 특성 때문에 오늘날 버퍼 초과 같은 보안 문제의 온상이 되어 있다. 가령, C 표준 함수 중에 strcpy는 잠재적으로 굉장히 위험한 함수이거니와, 파일도 아니고 키보드 입력을 받는데도 버퍼 크기 한계 지정도 없이 설계되어 있는 gets는 정말 도저히 그대로 쓸 수 없는 지경이 되어 있다.

이 문제를 해결하기 위해 윈도우 SDK에는 꽤 오래 전부터 StringCb* 함수들을 도입했다. write 버퍼의 포인터뿐만 아니라 버퍼 크기도 별도로 지정하여 복사가 그 영역 밖으로는 되지 않게, 즉 문자열이 잘리도록 조치를 취한 것이다. 이것들은 그냥 static 링크되는 코드이지 운영체제 커널 DLL에 있지는 않다는 점에서 lstrcpy 같은 함수와는 위상이 다르다. 사실 저 함수 자체가 기존 커널 l* 함수보다 더 안전한 솔루션을 제공하기 위해 추가된 것이다.

비주얼 C++ 2005부터는 보안 문제를 더욱 적극적으로 대처하는 함수군이 생겼다. 아예 _s 접미사가 붙은 strcpy_s 류이다. 이것도 하는 일은 기존 strcpy보다 더욱 안전하게 문자열을 복사하는 것이지만, 다음과 같은 면에서 StringCb*와는 동작 방식이 약간 다르며, 쓰임이 더 엄격하다는 것을 알 필요가 있다.

첫째, StringCb*는 버퍼 크기가 걸리면 문자열을 그냥 자르고 null-terminate까지 알아서 시켜 주는 반면, *_s는 런타임 에러를 발생시킨다. 그러므로 전자는 단순히 로그를 찍는 것과 같이 문자열이 꼭 다 복사되지는 않아도 되고 버퍼 초과 에러만 예방하고 싶을 때 쓰면 되며, 후자는 어떤 경우에도 버퍼가 초과하는 일 자체가 없이 문자열이 100% 정확하게 복사되는 게 보장되어야 할 때 쓰면 된다.

둘째, 특히 디버그 버전의 *_s 함수는 지정해 준 버퍼 영역을 다 검사한다. 비록 지금 한 글자밖에 복사할 게 없더라도 사용자가 100글자를 지정했다면 그 메모리가 다 안전한지 일일이 검사한다는 뜻이다. 그렇기 때문에
char a[8];
strcpy_s(a, 12, "ABC"); //12>8

는 비록 전혀 해롭지 않은 코드임에도 불구하고 에러를 일으킨다. 디버그 빌드에서는 말이다.

strcpy 같은 간단한 함수뿐만 아니라 scanf 함수도 쓰임이 강화되었다. 가령,
char s[64];
scanf_s("%s", s, 64);

이런 식으로 %s도 버퍼뿐만 아니라 버퍼의 크기까지 덧붙이도록 동작이 수정되었다는 뜻이다.

*_s 함수는 일부 템플릿 오버로드 버전도 존재하기 때문에, 기존 코드에다 *_s 함수를 넣는 리팩터링 작업을 할 때 일일이 배열 사이즈를 집어넣어야 하는 수고를 상당수 덜어 준다. 가령, 쓰기 버퍼가 포인터가 아니라 static 배열인 경우,

template<int N>
.... strcpy_s(char (&dst)[N], const char *src)
{
 return strcpy_s(dst, N, src);
}

이런 템플릿 오버로드가 이미 구비되어 있는 덕분에, strcpy를 strcpy_s로 바꾸기만 해도 알아서 배열의 크기가 전달된다. 물론 컴파일 타임에 말이다.
물론, 쓰기 버퍼가 임의의 포인터인 경우에는 이런 방법을 쓸 수 없고 사용자가 버퍼 크기를 수동으로 전달해 줘야 할 것이다.

본인도 어지간하면 이런 안전한 함수를 쓰고 싶은데, strchr/strstr 같은 함수의 결과값에다가 또 strcpy를 해야 할 때는 남은 버퍼 크기를 지정해 주는 게 좀 난감하다.

덧.
1. C언어에서 정적 배열의 원소 개수를 구할 때는 통상
#define ARRAYSIZE(x)  sizeof(x)/sizeof(x[0])
과 같은 형태로 정의하며, 이렇게만 써 놔도 이 식은 최적화 과정에서 모두 컴파일 타임 때 고정된 값이 계산되어 들어간다. 하지만 ARRAYSIZE 매크로도 템플릿을 이용한 매크로로 바꾸면 나눗셈 연산도 없고 더구나 배열이 아닌 일반 포인터를 넘겨주면 에러까지 나는(더욱 type-safe하기까지 한) 버전을 만들 수 있다.

템플릿으로 컴파일 타임 때 정말 별 걸 다 할 수 있다. ^^;; strcpy_s의 템플릿 오버로드 버전을 보니까 문득 이게 생각이 났다.

2. 아울러 어떤 구조체 멤버가 메모리 상으로 몇째 오프셋을 나타내는지 가리키는 매크로도 NULL 포인터로부터 ->로 참조한 멤버 값의 주소를 가리키는 방식으로 만들 수 있다. NULL 포인터는 -> 연산자를 적용하기만 하면 바로 에러가 날 것 같지만 주소 연산자 &는 메모리 위치에 상관없이 컴파일 타임 때 값이 계산되는 연산자이기 때문에 그런 일은 발생하지 않는다.

3. C/C++에서 0이라는 수치는 숫자와 포인터에 모두 아무런 형변환 없이 적용 가능한 녀석이어서 문제이다. true, false는 이미 진작부터 예약어가 생겼는데 C++도 nil이나 null 같은 예약어가 있어야 하지 않나 싶다. 본인은 전에도 이런 언급을 한 적이 있을 것이다.

Posted by 사무엘

2010/03/13 14:56 2010/03/13 14:56
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/210

비주얼 스튜디오로 C# 프로젝트를 하나 만든 후, 마법사가 생성해 준 기본 폼 애플리케이션을 debug와 release로 제각기 모두 빌드하여 실행해 본다.
그 후 이 프로젝트 디렉터리의 크기를 측정해 보라. 본인은 200KB가 채 나오지 않았다.

그런데 C/C++ 프로젝트를 만들고(MFC는 쓰지도 않고), 그냥 간단한 창만 하나 띄우는 Win32 프로그램을 debug와 release로 모두 빌드해서 실행한 후 디렉터리 크기를 재 보라.
프로젝트가 차지하는 용량은 무려 20MB가 넘는다. (비주얼 스튜디오 2008 기준)

그렇다. C/C++은 프로젝트를 만들고 빌드를 좀 하면, 잡다하게 생기는 중간(intermediate) 파일이 엄청 많다. 게다가 용량도 상당히 많이 잡아먹는다.

<날개셋> 한글 입력기 프로젝트도 32비트 디버그/릴리스, 그리고 64비트까지 빌드하다 보니 디렉터리 전체 크기가 무려 800MB에 달해 있다. 하지만 그 중 실제로 빌드에 쓰이는 소스나 데이터 파일의 합은 20~30MB대는 절대 안 넘을 것이다. -_-;;

OBJ 파일이 생기는 것이야 C/C++ 자체가 링크를 염두에 두고 만들어진 언어이니 어쩔 수 없고 그건 어차피 그렇게 크지도 않다. ..... 라고 말하려고 했는데, 오브젝트 파일도 각종 디버깅 내지 고급 최적화와 관련된 메타정보가 첨가되다 보면 단순히 소스 코드의 기계어 번역이라고 볼 수 없을 정도로 덩치가 은근히 커지긴 한다.

OBJ 말고도 디스크 용량을 상당히 차지하는 주범은 잘 알다시피 pre-compiled header이다(*.PCH) 겨우 몇몇 개의 헤더 정도나 인클루드하면 되는 정올 답안 수준의 프로그램이 아니라 특정 운영체제/플랫폼이나 거대한 라이브러리의 프로그래밍 요소를 다 인클루드하는 프로그램이라면, 그렇게 고정불변이고 덩치가 많은 요소들은 미리 컴파일을 좀 시켜 놔야지 프로그램의 빌드 시간을 줄일 수 있다.

본인이 비주얼 C++을 처음으로 쓴 게 4.2 시절부터이다. 그때엔 MFC 심볼들을 다 빌드해 놓은 pch 파일도 이미 3~5MB 정도 했던 것 같다. 하지만 지금은 그것보다 덩치가 훨씬 더 커져 있고 한 빌드 configuration당 10MB는 훌쩍 넘어간다. 프로젝트 하나 만들 때마다 50~100MB씩은 잡아야 한다. 오로지 C/C++ 언어 프로젝트만이 이런 삽질이 필요하다.

윈도우 SDK나 MFC처럼 매 프로젝트마다 일일이 빌드가 필요하지 않은 것들은 공용 PCH라는 개념으로 공유만 좀 하게 해 놔도 이런 파일의 크기를 상당 부분 줄일 수 있을 텐데 너무 낭비라는 생각이 든다. 하지만 요즘은 하드디스크 용량이 워낙 많다 보니, 빌드 시간을 네트워큭 분산 기술을 줄이려는 연구는 해도 PCH 파일 크기를 줄이려는 연구는 거의 행해지지 않는 것 같다.

이외에도 인텔리센스 데이터베이스인 NCB 파일도 은근히 크고, 매 빌드 때마다 생기는 심볼 디버그 데이터베이스인 PDB 파일도 무시 못 한다.
왜 이런 일이 생기는 것일까? 대략 다음과 같은 관점에서 살펴볼 수 있다.

첫째, C/C++은 굉장히 작은 언어이고, 프로그래밍에 필요한 요소들을 전적으로 소스와 동일한 텍스트 포맷인 헤더 파일의 파싱(느림!!)에 의존하여 읽어들이는 형태이기 때문이다. 라이브러리 링크는 별도의 계층으로 따로 존재하지, 즉시 읽어들일 수 있는 바이너리 유닛/패키지라든가(파스칼, 자바, C# 등) 언어가 자체 내장하고 있는 요소가 없다. 원천적으로 이식성을 택했지, 속도를 배려하지는 않은 느린 프로세스를 사용하는 셈이다.

둘째, C/C++은 전처리기라든가 링크 과정으로 인해 빌드가 더욱 느리고, 언어의 해석이 더디기 때문이다. 모든 토큰은 그냥 토큰이 아니라 전처리기를 재귀적으로 거치면서 다 까 봐야 실체가 드러난다. ^^;;; 헤더 파일의 글자 하나를 고치면 이 여파가 수백, 수천 개의 소스 파일에 동시에 파급되고 프로그램의 의미가 전혀 다르게 바뀔 수 있다. 이것은 장점도 있지만 똑똑한 개발 환경이나 빠른 빌드 환경을 만드는 데는 불리한 구조이다.
그러니, 소스 코드를 조금이라도 빨리 분석하기 위해서는 소스코드 자체뿐만 아니라 온갖 메타정보들을 ‘별도의 파일’로 보관할 수밖에 없다. NCB처럼 말이다.

셋째, C/C++은 그 어느 에뮬레이터나 가상 기계 계층이 없이, CPU 차원에서 기계가 직통으로 알아듣는 프로그램을 만들 수 있다. 잘 최적화된 프로그램은 사람이 원래 짠 소스 코드와는 전혀 다른 형태가 될 수도 있다. 그러니 이런 코드의 디버깅은 어려울 수밖에 없다. 변수의 내용을 확인하거나 한 줄씩 실행하는 것까지는 못 하더라도 프로그램이 뻗었을 때 스택 덤프라도 보려면 빌드된 프로그램과 소스 코드 사이의 관계를 설명하는 최소한의 정보라도 있어야 한다. 소스 코드와 형태가 전혀 딴판인 코드를 생성하는 컴파일러일수록 그런 정보는 더욱 세부적이고 양이 많아질 수밖에 없다.

한 마디로 C/C++이 정말 강력하고 포괄적이고 대인배 같은 언어이다 보니 주변에 붙는 군더더기도 많은 모양이다. ^^ 코드 생성이 여러 단계를 거치면서 매우 번거롭고 어려운 대신, 한번 만들어 진 코드는 그 어느 언어보다도 강력하다.

Posted by 사무엘

2010/02/22 21:40 2010/02/22 21:40
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/194

« Previous : 1 : ... 4 : 5 : 6 : 7 : 8 : 9 : Next »

블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2026/07   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

Site Stats

Total hits:
3944278
Today:
1572
Yesterday:
1689