1.

본인은 비주얼 C++ 2012로 갈아탄 뒤부터 예전에는 본 적이 없는 이상한 현상을 겪곤 했다. 내가 만들고 있는 프로그램을 IDE에서 곧장 실행하자(Ctrl+F5 또는 F5) 프로세스는 분명히 실행되어 있는데 창이 화면에도, 작업 표시줄에도 전혀 나타나 보이지 않았다.

Spy++를 돌려 보니 프로그램 창이 생기긴 생겼는데 어찌 된 일인지 WS_VISIBLE 스타일이 없이 숨겨져 있다는 걸 알게 되었고, 문제의 원인은 생각보다 금방 발견할 수 있었다.
프로세스에 전달되는 STARTUPINFO 구조체의 wShowWindow 멤버 값은, dwFlags에 STARTF_USESHOWWINDOW 플래그가 있을 때에만 유효하다는 걸 깜빡 잊고 있었던 것이다.

일반적으로 프로그램을 실행할 때 운영체제가 그 구조체에다 ShowWindow 플래그를 안 넣는 적은 사실상 없기 때문에 지금까지 그 로직이 별로 문제가 되지 않았었다. 하지만 비주얼 C++ 2012는 이례적으로 그 구조체의 거의 모든 멤버들을 그냥 0으로만 집어넣은 채 프로세스를 생성하고, 0은 SW_HIDE와 같기에 창이 화면에 나타나지 않았다.

2.

<날개셋> 한글 입력기 외부 모듈을 debug 형태로 빌드한 뒤 디버거를 붙여서 실행해 보면, 때에 따라서는 호스트 프로세스가 종료될 때 memory leak 로그가 뜨는 경우가 종종 있었다. 하지만 이것이 항상 나타나는 건 아니고 leak의 양이 심각하게 많은 건 아니었기 때문에, 본인은 크게 신경 쓰지는 않았다.

그런데 우연히 추가 디버깅을 한 결과, 응용 프로그램에 따라서 아예 COM 개체들의 reference count가 달라지고 TSF 모듈의 소멸자 함수의 실행 여부가 달라지는 걸 발견하였고, 이에 본인은 이 현상에 대해 좀 더 심혈을 기울여 디버깅을 실시하게 되었다.

이건 꽤 특이한 현상이었다. <날개셋> 편집기에서도 leak이 발생했기 때문에 가장 먼저 'TSF A급 지원' 옵션을 꺼 봤다. 그리고 외부 모듈은 아예 날개셋 커널을 로딩하지 않고 아무 기능도 사용할 수 없는 panic 상태로 구동했다. 그렇게 프로그램의 주요 기능들을 다 끄고 절름발이로 만들었는데도 <날개셋> 외부 모듈을 한 번이라도 로딩을 하고 나면 leak이 없어지지 않았다.

이런 식으로 COM 오브젝트의 reference count가 꼬이는 버그는 여간 골치 아픈 문제가 아니기에 각오 단단히 하고 디버깅을 계속할 수밖에 없었다. 그 결과 무척 신기한 점을 발견했다. MFC를 사용하는 GUI 프로그램과, MFC든 무엇이든 대화상자(DialogBox)를 사용하는 프로그램에서는 leak이 안 생기는데, Windows API로 message loop을 직접 돌리면서 윈도우를 구동하는 프로그램에서는 memory leak이 발생한다는 것이었다.

오히려 방대하고 복잡한 MFC를 쓰는 프로그램에서 메모리가 새면 샜지, 왜 더 간단한 프로그램에서 문제가 발견되는 걸까?
이 정도까지 밝혀지니 궁금해 미칠 지경이 됐다. leak이 있는 프로그램과 없는 프로그램을 종료할 때 외부 모듈 개체의 Release 함수가 어떻게 호출되고 reference count가 어떻게 변하는지를 검토했다.

그리고 드디어 leak이 있는 프로그램과 없는 프로그램의 차이가 밝혀졌다.
MFC는 프로그램 창이 WM_CLOSE 메시지를 받아서 창의 소멸 단계로 들어서기 전에, 프로그램 창을 강제로 한번 감춰 주고 있었다( ShowWindow(SW_HIDE) ). CFrameWnd::OnClose()에서 CWinApp::HideApplication을 호출함. 이걸 함으로써 운영체제의 TSF 시스템 내부는 객체에 대한 Release가 일어나고 메모리 해제가 완전히 이뤄졌다. 소스가 없는 대화상자도(DialogBox 함수) 잘은 모르지만 종료될 때 비슷한 call stack을 갖는 Release 호출이 있었다.

그 반면 창이 없어질 때 따로 별다른 처리를 하지 않는 프로그램에서는 외부 모듈 개체의 reference count가 1 남게 되었고, 이것이 memory leak으로 이어졌다. MS에서 직접 만든 다른 입력 프로그램들도 마찬가지다. 도대체 왜 그럴까?.

MFC가 WM_CLOSE에서 자기 창을 감추는 이유는 그냥 자식 윈도우들이 순서대로 닫히는 모습이 사용자에게 티가 나 보이지 않게 하고, 겉보기로 창이 당장 없어져 버렸으니 프로그램 종료에 대한 사용자 반응성을 향상시키려는 목적으로 보인다. 그게 반드시 필수는 아니다. 내가 보기에 그렇게 하지 않는 게 잘못이라 볼 수는 없다.

OS별로 살펴보니, 이런 leak은 윈도우 XP와 비스타에서는 없었다가 그 후대인 7과 8에서 생겼다. 즉, XP/Vista에서는 hide를 안 해 줘도 원래 leak이 없는데 7부터는 hide를 해 줘야 한다는 뜻. 아무튼 난 여러 모로 윈7의 문자 입력 체계가 별로 마음에 안 든다. 이쪽 부분 담당자가 갑자기 바뀌었는지, 혹은 대대적인 리팩터링을 한 후유증이기라도 한지 자잘한 버그들이 너무 많이 들어갔기 때문이다.

결국 이것은 IME 문제가 아니라 운영체제 내지 응용 프로그램의 문제라는 결론을 내리고 편집기의 소스를 고쳤다. 문제를 피해 가는 법을 발견하긴 했으나 뒷맛이 개운하지 못하다.

* Windows 환경에서의 4대 디버깅 도구와 테크닉

  • 문자열을 printf 스타일로 포맷하여 OutputDebugString 함수로 전달하는 TRACE 함수 (디버거 로그)
  • 별도의 디버거 로그가 아니라 그냥 화면 desktop DC에다가 로그를 찍는 깜짝 함수
  • 프로그램이 특이한 환경에서 뻗을 때 call stack을 확인할 수 있는 miniDumpWriteDump와 SetUnhandledExceptionFilter 함수
  • memory allocation number에다가 breakpoint를 거는 _crtBreakAlloc 변수. 정체불명의 memory leak 잡을 때 필수

Posted by 사무엘

2013/03/02 19:24 2013/03/02 19:24
, , ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/802

Trackback URL : http://moogi.new21.org/tc/trackback/802

Comments List

  1. wafe 2013/03/03 02:12 # M/D Reply Permalink

    오호... 신기한 일일세. win7도 그렇지만 win8에서는 더더욱 크게 입력기 쪽이 바뀐 것 아닌가 싶은데, 그래서인지 새나루나 날개셋이나 정상적으로 동작하지 않게 되어서 참 아쉽구먼.

    1. 사무엘 2013/03/03 04:44 # M/D Permalink

      Windows 8은 저같은 프로그래머를 상당히 번거롭게 한 변화가 많았지요. =_=
      참고로 <날개셋> 한글 입력기는 지난달에 나온 6.8 버전이 Win8을 Modern UI까지 제대로 지원하기 시작했답니다.

  2. Lyn 2013/03/11 11:32 # M/D Reply Permalink

    전 어차피 UI는 안만지다 보니 무조건 안정적이고 성능만 빠른 걸 찾게되네요 ㅎㅎ

    그런의미에서 8/2012는 대만족

    1. 사무엘 2013/03/11 13:48 # M/D Permalink

      똑같이 프로그래머라 해도 종사하는 분야는 너무 다양하고 많고.. 넓죠.
      저도 비주얼 C++ 2012의 IDE와 코딩 편의 기능은 대만족이에요. 몇몇 다운그레이드된 기능들, 변경된 컴파일 방식 때문에 불편하지만 2010을 완전히 언인스톨은 못 하겠지만, 201x가 2005나 2008은 완전히 대체 가능한 것 같습니다.

      윈8은 지금으로부터 최하 1~2년은 뒤에 노트북이나 데스크톱 컴퓨터를 새로 장만할 때나 가상 기계가 아닌 main으로 쓰게 될까요?
      어쩌면 곧바로 윈9로 갈아타게 될지도?

Leave a comment

C++의 for each, in 키워드

심각한 뒷북인지 모르겠는데,
본인은 비주얼 C++에서 이런 문법이 가능하다는 걸 아주 최근에야 알게 되었다.

DATA container[N];

for each(DATA elem in container) {
    do_with(elem);
}

저건 언어 차원에서 제공하는 새로운 문법이기 때문에 STL <algorithm>의 for_each 함수와는 다르다.

배열을 순회하기 위한 별도의 임시 변수(일회용 int i나 거추장스러운 포인터)를 선언할 필요 없이, 코드를 굉장히 깔끔하게 만들 수 있어서 좋다. 이것의 주 용도는 C++을 상당한 고수준 언어로 끌어올린 C++/CLI 환경이지만, 네이티브 환경에서도 정적 배열과 STL 컨테이너 정도에서는 아주 요긴하게 쓰일 수 있다.

DATA가 int 같은 아주 기본적인 자료형이라면 그냥 저렇게 써 주면 되고, 개당 수십~수백 바이트씩 하는 무거운 구조체라면 const DATA&를 하면 된다. 그리고 순회 중인 배열 자체의 데이터를 루프 안에서 고쳐야 한다면 물론 DATA&라고 써 주면 된다.
저건 C++11 같은 급도 아니고, 생각보다 굉장히 오래 된 비주얼 C++ 2005에서부터 지원되기 시작했다고 한다.

컴파일러가 언어 표준에 없는 변칙 문법이나 키워드를 지원하는 것은 특정 CPU나 운영체제에 종속적인 기능을 추가로 제공하기 위해서이다.
하지만 for each는 그런 범주에 속하지 않으며, 전통적인 C/C++ 언어의 토큰 나열과 비교했을 때 문법도 굉장히 이질적이다. 그럼에도 불구하고 비주얼 C++이 이것을 제공하는 것이 신기하기 그지없다.

그리고 또 하나 생각할 점은, 저기서 each와 in은 문맥 의존적인 임시 예약어(키워드)라는 것이다. for 다음에 이어졌을 때만 키워드이며, 다른 곳에서는 사용자가 each나 in을 일반적인 변수/함수명으로 얼마든지 쓸 수 있다는 뜻.

언어 설계 차원에서 C/C++은 원래 임시 예약어라는 게 없는 언어이다. 한번 예약어로 찜해진 단어는 그 어떤 곳에서도 명칭으로 결코 쓰일 수 없다. 다른 구문이나 수식을 파싱하는 데는 문맥 의존적인 어려운 문법이 많지만, 예약어 식별만은 단순하게 만들려고 했는가 보다.

그 반면, 파스칼은 begin, end, if, for 같은 단어야 절대적인 예약어이겠지만 forward(함수 전방 선언용)를 포함해 몇몇 키워드는 일정 문맥에서 별도의 의미를 갖는 임시 예약어이다. 그리고 객체지향 개념이 추가된 오브젝트 파스칼의 경우 virtual 같은 함수 modifier, 그리고 클래스 내부에서 public/protected 같은 멤버 접근성 modifier도 임시 예약어이다. C++은 그렇지 않다.

비주얼 C++은 for each, in뿐만 아니라 abstract, override, delegate 등 몇몇 비표준 임시 예약어를 더 두고 있기도 한데, 이것은 대개가 C++/CLI용이고 네이티브 환경에서는 쓰일 일이 별로 없다. 일반적인 경우라면 비표준 확장 예약어는 앞에 __를 붙여서 명칭 충돌의 여지를 없앤 뒤에 절대적인 예약어로 추가하는 게 관행일 텐데, 저것들은 그렇게 하지 않았다.

끝으로, for each, in에다가 2차원 배열을 넘겨 주면 어떻게 될까 궁금해서 시도를 해 봤는데, 이때도 각 원소들이 하나씩 순서대로 순회되더라.
각 배열을 배열의 포인터로 받으면서 1차원적으로 순회되지는 않는가 보다.

비주얼 C++ 2010은 인텔리센스 컴파일러와 실제 컴파일러의 동작이 서로 다르기라도 했는지, IDE에서는 이때 each 변수가 2차원 배열과 서로 호환이 되지 않는다고 빨간줄 에러를 뱉은 반면, 실제 컴파일은 됐다.
2012에서는 그것이 개선되어 IDE에서도 빨간줄이 생기지 않는다.

Posted by 사무엘

2013/02/16 08:17 2013/02/16 08:17
, , ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/796

Trackback URL : http://moogi.new21.org/tc/trackback/796

Comments List

  1. 김진 2013/02/16 12:01 # M/D Reply Permalink

    C#에만 있는 문법인 줄 알았는데 C++에서도 쓸 수 있었군요. 배열 순회도 될 텐데, C#에서는 배열의 배열int[2][3]과 2차원배열int[2,3]을 구별하니까 그 영향이 있을 것 같네요.
    자바에도 이게 있는데 코드를 깔끔하게 만들어 주기도 하지만, 엉뚱한 인덱스 변수를 쓰거나 값이 배열 범위 바깥으로 벗어나는 사태를 원천봉쇄한다는 점이 좋더군요.

    1. 사무엘 2013/02/16 19:33 # M/D Permalink

      오랜만에 뵙네요. ^^
      네, 아주 C#스러운 문법이죠. 아주 안전하고 편리하긴 하나, 남발하면 나의 C++ 코드도 점점 MS 컴파일러에 종속될 것 같습니다.
      그나저나 C#은 배열의 배열과 다차원 배열을 모두 표현할 수 있는 게 마치 Ada 언어와 비슷하다는 생각이 들었습니다. ^^

    2. 김진 2013/02/18 01:08 # M/D Permalink

      찾아보니 C++11표준에 이게 있었네요. gcc나 clang에서는 이런 형식으로 쓰더군요:
      DATA container[N];
      for (auto elem : container) {
      do_with(elem);
      }
      표준이니 앞으로는 모든 컴파일러에서 지원하겠네요.^^

    3. 사무엘 2013/02/18 10:00 # M/D Permalink

      보충 설명에 감사드립니다. ^^
      C++에서 그런 문법을 정식 도입하려면 저렇게 기호를 쓰지 each, in 같은 거추장스러운 영단어 나열을 쓰지는 않을 겁니다. 그건 C++의 철학이 아니라 생각됩니다.
      다만, for의 한참 다음에 ; 이 나오느냐 : 이 나오느냐를 계속 토큰을 살펴봐야 전통적인 for인지 새로운 for인지 파악이 될 테니, 파싱은 좀 어려울 것 같습니다.

Leave a comment

템플릿 인자로 또 템플릿 타입을 받는 타입의 변수 선언이

A<B<C > > d;
이런 식으로 돼 있는 옛날 C++ 코드를 보니 문득 감회가 새롭다.

예전에는 템플릿 인자를 닫는 > 가 중첩될 때, 여러 >를 >>로 붙일 수가 없었다.
타입 선언인지 일반 연산인지 문맥을 고려하지 않는 전통적인 parser는, 이것을 비트 shift 연산자로 인식하기 때문이었다. 따라서 오류크리.
그래서 > 사이를 강제로 띄워 줘야 했는데 이것이 보기에 그리 좋지는 않음이 자명한 노릇이었다.

일단 C++ 계보의 언어들은 문법 차원에서 변수 선언을 명시하는 토큰이 없고(파스칼의 var과 콜론, 베이직의 Dim과 as 같은), 달랑 “타입 변수명”이라는 아주 문맥 의존적인 문법만을 바탕으로 변수 선언을 컴파일러가 알아서 추론해야 하기 때문에 파싱이 까다로운 게 사실이다. 게다가 C++부터는 변수 선언은 객체 선언과 동급이 되어, 함수 몸체 내부 어디에서나 마음대로 올 수 있지 않은가.

훗날 C++ 언어가 C++11로까지 확장되면서, 언어가 명시하는 스펙 자체가 바뀌면서 >>를 붙여 써도 괜찮게 되었다.
비주얼 C++의 경우, 2003은 >>가 확실하게 인식되지 않았는데, C++11이 정식으로 제정되기 전부터 2008쯤부터 이미 >>를 지원하고 있었다.

이런 문법의 변화로 인해, 클래스 A는 type을, 클래스 B는 int를 받는 템플릿 클래스라고 했을 때

A<B<30>>1> > p;

라는 코드가 과거에는 30>>1이 15라고 계산되어 컴파일이 되었지만, 이제는 되지 않는다. >>가 템플릿 인자를 닫는다는 의미로 먼저 인식되었기 때문이다. 이것은 함수 호출 문맥에서는 ,가 콤마 연산자가 아니라 인자 구분자로 먼저 인식되는 것과 비슷한 맥락이다.
바뀐 문법에서는

A<B<(30>>1)>> p;

라고, 뒤의 >를 붙일 수 있는 대신 진짜 템플릿 인자 내에서의 산술 연산은 괄호로 싸 줘야 <, > 사이의 모호성을 막을 수 있다.
사실, 템플릿 인자 안의 숫자는 어차피 컴파일 시점에서 값이 다 결정되는 것들이기 때문에, 복잡한 연산이 들어갈 일은 거의 없다. 산술 연산을 괄호로 반드시 싸야 하게 만들고 그 대신 템플릿 인자의 < >에 편의를 더 주는 것이 훨씬 더 합리적인 정책인 것이 사실이다.

뭐, 괄호도 해 주고 >를 띄워 주기까지 하면, 어느 구닥다리 C++ 컴파일러에서나.. 컴파일 가능한 코드를 만들 수 있긴 하지만, 미관은 제일 떨어지겠지. ㅋㅋ

그러고 보니 옛날에는 일반 함수 포인터 말고, C++ 멤버 함수 포인터를 명시할 때 그냥 이름만 써 줘도 괜찮은 수준이었는데
나중에는 반드시 &를 붙이고 scope도 명시해 줘야 하게 문법이 좀 더 엄격하게 바뀐 걸로 기억한다. 한 VC++ 2005쯤부터이다. for(int x=0; ... )에서 x의 scope만큼이나 전형적인 호환성 문제이다.

이렇듯 C++이 어제나 오늘이나 큰 뼈대는 변함없고 계속 새로운 기능이 추가만 되는 것 같아도,
이미 있던 문법도 야금야금 바뀌어 온 게 좀 있다.

Posted by 사무엘

2012/12/10 08:30 2012/12/10 08:30
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/767

Trackback URL : http://moogi.new21.org/tc/trackback/767

Comments List

  1. Lyn 2012/12/10 16:25 # M/D Reply Permalink

    표준 만들어지는데 10년

    표준에 맞춘 컴파일러가 웬만큼 깔려서 새 기능을 일상적으로 쓸수 있게 되는데 10년

    그러나 그때쯤이면 새 표준이 나오겠지...
    안될거야 아마

    1. 사무엘 2012/12/10 17:38 # M/D Permalink

      그래도 가끔은 비주얼 C++에서 보듯이 컴파일러가 표준을 먼저 수용하기도 하지요. VC2010도 C++11이 정식 확정되기 전에 일부 규격을 수용한 거니까요.

      C#도 D도 쓰고 싶긴 합니다만, 저는 이미 인생의 방향을 (거의) 결정해 버린 대형 C++ 프로젝트를 안고 가는 개발자.. ㅎㅎㅎ

  2. 닌자 2012/12/10 18:49 # M/D Reply Permalink

    밑에 박정희 대통령에 대한 글에 조금 설명할 것이 있어서 댓글을 달고 싶은데 댓글을 달 수 없어서 여기에 답니다.

    아래 링크 주소는 박정희에 대한 일본 위키백과 내용입니다.

    http://ja.wikipedia.org/wiki/%E6%9C%B4%E6%AD%A3%E7%85%95

    위에 써 있는 내용 가운데 흥미로운 점이 있어서 알려드릴까 합니다.

    일본어를 할 줄 아시면 바로 해석이 가능하시겠지만 제 나름대로 해석을 하면

    第二次世界大?中の1944年に日本の陸軍士官?校を3位の成績で卒業(57期)し、終?時は?州?軍中尉だった。朴正?が日本陸軍の軍人だったという誤解があるが、上記のように?州?の軍人として日本の士官?校への留?を命じられたに過ぎず、日本軍人として任官したことはない。

    제2차 세계대전 중이던 1944년에 일본의 육군사관학교를 3위 성적으로 졸업(57기)하고, 종전 시에는 만주 국군 중위였다.
    박정희가 일본 육군의 군인이었다는 오해가 있지만 상기한 바와 같이 만주군의 군인으로서 일본 사관학교에 유학을 명 받은 것일 뿐 일본군으로서 임관 된 적은 없다.

    이렇게 나옵니다.

    즉 최소한 박정희는 저 글대로만 본다면 정규 일본 육군이 아니었단 말이죠.
    정확하게 말하면 일본 육사 졸업 후 만주국의 육군 소위로 임관 한 것이다 라고 말할 수 있겠습니다.

    창씨개명이야 그 당시 살던 사람이라면 어쩔 수 없이 하던 선택이고 김용묵님도 사무엘이라고 자발적으로 창씨개명을 하셨지만 요즘 외국식 이름으로 바꾼다고 그게 무슨 흉이 되나요?

    나중에 미국하고 사이가 안 좋아지면 100년 후 미국식으로 이름 바꾼 놈들은 다 매국노야 이렇게도 될 수 있겠네요?

    김용묵님 덕분에 세벌식 잘 쓰고 있습니다.

    스마트폰용 세벌식(가급적이면 390식!!)도 있으면 좋겠지만 개발을 강요 하지는 않을게요. 너무 힘든 일이라 말이죠.
    하지만 있으면 좋을 것 같아요.

    1. 사무엘 2012/12/10 21:11 # M/D Permalink

      반갑습니다. 그리고 보충 설명에 감사드립니다.. ^^
      그 설명대로라면 박 정희는 제가 생각한 것만큼도 일본과 가까이 관여하는 직위가 아니었다는 얘기군요.
      하지만 뭐든 중상모략은 빨리 퍼져나가고 수습은 무지하게 더딘가 봅니다.
      박 정희뿐만 아니라 이 승만에 대해서도.. 제 개인적인 생각은 이젠 제발 좀 “6· 25 때 자기 혼자 튀었다” 같은 헛소리 좀 안 듣고 살아 보는 게 소원입니다.

      그 글은 저와 견해가 다른 건 상관 안 하는데, 글을 제대로 읽지 않고 그냥 자기 감정 배출용으로 올라오는 딴지성 댓글을 상대하기가 귀찮아서 댓글 등록을 막아 놨습니다.

      그리고 마지막 문단을 보면서 그저 웃습니다.
      당장 현실적으로는 윈도우 8 메트로 지원이 더 급한 문제랍니다.. ^^;;

    2. 박상대 2012/12/10 22:36 # M/D Permalink

      스마트폰용 세벌식은 지금도 있습니다.

      "MN 로그인 키보드" 라는 앱에서 세벌식 최종 자판과 신세벌식 자판을 지원하고 있습니다.

      하지만 무료버전에서는 자판 위에 광고가 뜬다는 단점이 있습니다.
      단, 세벌식 최종 자판만은 자판이 네 줄이라서 예외적으로 광고가 안 뜹니다.

Leave a comment

C++의 템플릿에서 인자로 쓰이는 것은 정수 아니면 자료형이다. 자료형은 class 또는 typename으로 명시해 줄 수 있으며, 이 자료형 인자는 (1) 클래스 내부의 멤버 변수의 자료형, 또는 (2) 멤버 함수의 인자나 리턴값의 자료형으로 쓰일 수 있다.

template<typename T>
class Foo {
public:
    T Bar;
};

그러니 위와 같이 생긴 클래스는 Foo<int>, Foo<char *>, Foo<RECT> 등 여러 형태로 활용할 수가 있는데,
이건 뭐지..?

Foo<int(PCSTR)> f;

이것은 int (*)(PCSTR)처럼 함수의 포인터를 지정한 것도 아니고, 일반적인 상황에서는 있을 수 없는 타입 문자열이다.
이것은 템플릿 인자에서만 허용되는 문법인데, 클래스의 멤버 함수의 프로토타입을 지정한 것이다. 이렇게 선언된 클래스에서는 Bar가 멤버 변수가 아니라 아래와 같이 호출 가능한 멤버 함수가 된다! 클래스의 형태가 완전히 달라지게 된다.

int x = f.Bar("hello, world!");

물론, Bar 함수의 몸체는 사용되는 템플릿 인자별로 모두 정의를 해 줘야 한다. 안 그러면 링크 에러가 난다.

template<>
int Foo<int(PCSTR)>::Bar(PCSTR p)
{
    return (int)p;
}

결국 멤버 함수의 인자와 리턴값이 템플릿의 인자에도 들어가고 함수 자체에도 중복 기재되는 셈이다.

Bar에 대해서 f.Bar()처럼 함수 호출이 가능하려면 Bar는 ()연산자가 오버로드되어 있는 클래스 개체이거나, 함수 포인터 타입이거나 함수 포인터로 형변환이 가능한 클래스 개체여야 한다.
그런데 그에 덧붙여 클래스 멤버 문맥에서는 위와 같은 멤버 함수 선언도 들어갈 수도 있으니, C++의 템플릿은 정말 귀에 걸면 귀걸이, 코에 걸면 코걸이가 아닐 수 없다. 심지어 virtual int(PCSTR) 같은 가상 함수 선언도 가능하다!

export 키워드가 괜히 백지화된 게 아님을 느낀다. 템플릿은 워낙 너무 방대한 언어 규격이기 때문에, 템플릿의 몸체를 다른 번역 단위로부터 끌어다 쓸 수 있으려면 템플릿으로 할 수 있는 일의 범위를 좀 더 좁혀야 할 것이다.

그런데 저렇게 멤버 함수를 완전히 customize하는 문법은, 단순히 신기한 것 이상으로 활용 방안이나 유용한 구석이 있는지 잘 모르겠다. 내가 C++ 프로그래밍을 10년이 넘게 해 왔지만, 템플릿으로 저런 것까지 가능하다는 걸 알게 된 건 1년이 채 되지 않았다.
비주얼 C++ 2003도 저게 가능할 정도이니 이건 최신 문법은 아닌 게 분명해 보인다. 그 반면 xcode에서는 이게 지원되지 않는다.

함수 개체를  함수의 인자로 전달할 때는 전통적인 함수 포인터뿐만이 아니라 C++11에서 추가된 람다 함수 오브젝트를 손쉽게 넘겨 줄 수 있다. 이때 템플릿이 아주 유용한 역할을 한다. 가령, 정렬 함수를 호출할 때 비교 함수를 익명 함수로 간단히 알고리즘을 짜서 전해 주면 되니 얼마나 편리한지 모른다.

그 반면 클래스가 함수 오브젝트를 멤버로 받는 건 아무 의미가 없고 가능하지도 않다. 그 대신 클래스 멤버가 템플릿일 때는 이것이 멤버 변수도 되고 아예 멤버 함수도 될 수 있는 자유도가 제공된다고 이해하면 되겠다.

Posted by 사무엘

2012/12/01 19:21 2012/12/01 19:21
,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/763

Trackback URL : http://moogi.new21.org/tc/trackback/763

Comments List

  1. 김재주 2012/12/02 14:07 # M/D Reply Permalink

    이미 C++이란 언어는 그 자체로 너무 복잡한데다
    스크립트 언어들의 성능도 무시무시한 속도로 좋아지고 있으니 언제까지 써먹을지 싶습니다.

    제가 요즘 관심있게 보는 언어는 얼랭입니다. 간단하면서도 표현력과 성능이 모두 좋은 언어들이 많죠.

    1. 사무엘 2012/12/03 00:01 # M/D Permalink

      아무리 모바일 기기가 대세여도 PC 자체가 결코 없어질 수는 없듯이,
      C++도 차지하는 비중이 줄어들기만 할 뿐, 아마 없어지지는 않을 겁니다.
      네이티브 코드 생성 지향 언어로서는 가히 독자적인 지위를 차지하고 있잖아요?

      다만, 획기적인 새로운 표준에 의해 C++ 자체의 모습이 훗날 확 달라질 가능성은 있을 것 같습니다. 예를 들어 #include 대신 언어 차원에서 패키지 규격이 추가된다거나. ㄲㄲㄲ

  2. 김재주 2012/12/07 20:21 # M/D Reply Permalink

    근데 결국 다른 언어들이 C++의 성능마저 따라잡는 날이 어느 순간엔가는 올 거거든요.
    지금도 어지간한 언어는 LLVM을 통해 최적화된 네이티브 코드를 생성할 수 있고요. (C++ 자신조차도!)

    또 JIT 컴파일러 기술이 발전하다보면 VM 언어들의 성능 문제를 생각하지 않아도 될 수 있겠죠.


    스마트폰과 같은 모바일 기기의 성능이 PC를 능가할수는 없을 것이 분명한데, 이를 최적화된 코드 생성 능력만으로 커버할 수 있을까요? 제가 보기엔 단일 기기에서의 성능이 얼마나 좋은가보다는 결국 클라우드 컴퓨팅 파워를 얼마나 편리하게, 효율적으로 활용하느냐가 중요해질 것이거든요. C++은 이런 점에선...

Leave a comment

문자의 집합인 문자열(string)은 어지간한 프로그래밍 언어들이 기본으로 제공해 주는 기본 중의 기본 자료형이지만, 그저 기초라고만 치부하기에는 처리하는 데 내부적으로 손이 많이 가기도 하는 자료형이다.

문자열은 그 특성상 배열 같은 복합(compound) 자료형의 성격이 다분하며, 별도의 가변적인 동적 메모리 관리가 필요하다. 또한 문자열을 어떤 형태로 메모리에 저장할지, 복사와 대입은 어떤 형태로 할지(값 내지 참조?) 같은 전략도 구현체에 따라서 의외로 다양하게 존재할 수 있다.

그래서 C 언어는 컴퓨터 자원이 열악하고 가난하던 어셈블리 시절의 최적화 덕후의 정신을 이어받아, 언어 차원에서 따로 문자열 타입을 제공하지 않았다. 그 대신 충분히 크게 잡은 문자의 배열과 이를 가리키는 포인터를 문자열로 간주했다. 그리고 코드값이 0인 문자가 문자열의 끝을 나타내게 했다.

그 이름도 유명한 null-terminated string이 여기서 유래되었다. 오늘날까지 쓰이는 역사적으로 뿌리가 깊은 운영체제들은 응당 어셈블리나 C 기반이기 때문에, 내부 API에서 다 이런 형태의 문자열을 사용한다.
그리고 파일 시스템도 이런 문자열을 사용한다. 오죽했으면 이를 위해 MAX_PATH (=260)같은 표준 문자열 길이 제약까지 있을 정도이니 말 다 했다. 그렇기 때문에 null-terminated string은 앞으로 결코 없어지지 않을 것이며 무시할 수도 없을 것이다.

딱히 문자열만을 위한 별도의 표식을 사용하지 않고 그저 0 문자를 문자열의 끝으로 간주하게 하는 방식은 매우 간단하고 성능면에서 효율적이다. 지극히 C스러운 발상이다. 그러나 이는 buffer overflow 보안 취약점의 근본 원인을 제공하기도 했다.

또한 이런 문자열은 태생적으로 문자열 자기 내부엔 0문자가 또 들어갈 수 없다는 제약도 있다. 하지만 어차피 사람이 사용하는 표시용 문자열에는 코드 번호가 공백(0x20)보다 작은 제어 문자들이 사실상 쓰이지 않기 때문에 이는 그리 심각한 제약은 아니다. 문자열은 어차피 문자의 배열과는 같지 않은 개념이기 때문이다.

문자열을 기본 자료형으로 제공하는 언어들은 대개 문자열을 포인터 형태로 표현하고, 그 포인터가 가리키는 메모리에는 처음에는 문자열의 길이가 들어있고 다음부터 실제 문자의 배열이 이어지는 형태로 구현했다. 그러니 문자열의 길이를 구하는 요청은 O(1) 상수 시간 만에 곧바로 수행된다. (C의 strlen 함수는 그렇지 않다)

그리고 문자열의 길이는 대개 machine word의 크기와 일치하는 범위이다. 다만, 과거에 파스칼은 이례적으로 문자열의 크기를 16비트도 아닌 겨우 8비트 크기로 저장해서 256자 이상의 문자열을 지정할 수 없다는 이상한 한계가 있었다. 더 긴 문자열을 저장하려면 다른 특수한 별도의 자료형을 써야 했다.

과거에 비주얼 베이직은 16비트 시절의 버전 3까지는 “포인터 → (문자열의 길이, 포인터) → 실제 문자열”로 사실상 실제 문자열에 접근하려면 포인터를 이중으로 참고하는 형태로 문자열을 구현했다. 어쩌면 VB의 전신인 도스용 QuickBasic도 문자열의 내부 구조가 그랬는지 모르겠다.

그러다가 마이크로소프트는 훗날 OLE와 COM이라는 기술 스펙을 제정하면서 문자열을 나타내는 표준 규격까지 제정했는데, COM 기반인 VB 4부터는 문자열의 포맷도 그 방식대로 바꿨다.

일단 기본 문자 단위가 8비트이던 것이 16비트로 확장되었다. 마이크로소프트는 자기네 개발 환경에서 ANSI, wide string, 유니코드 같은 개념을 한데 싸잡아 뒤죽박죽으로 재정의한 것 때문에 문자 코드 개념을 좀 아는 사람들한테서 많이 까이고 있긴 하다. 뭐, 재해석하자면 유니코드 UTF16에 더 가깝게 바뀐 셈이다.

OLE 문자열은 일단 겉보기로는 null-terminated wide string을 가리키는 포인터와 완전히 호환된다. 하지만 그 메모리는 OLE의 표준 메모리 할당 함수로만 할당되고 해제된다. (아마 CoTaskMemAlloc) 그리고 포인터가 가리키는 메모리의 앞에는 문자열의 길이가 32비트 정수 형태로 또 들어있기 때문에 문자열 자체가 또 0문자를 포함하고 있을 수 있다.

그리고 문자열의 진짜 끝부분에는 0문자가 1개가 아니라 2개 들어있다. 윈도우 운영체제는 여러 개의 문자열을 tokenize할 때 double null-termination이라는 희대의 괴상한 개념을 종종 사용하기 때문에, 이 관행과도 호환성을 맞추기 위해서이다.

2중 0문자는 레지스트리의 multi-string 포맷에서도 쓰이고, 또 파일 열기/저장 공용 대화상자가 사용하는 확장자 필터에서도 쓰인다. MFC는 프로그래머의 편의를 위해 '|'(bar)도 받아 주지만, 운영체제에다 전달을 할 때는 그걸 다시 0문자로 바꾼다. ^^;;;

요컨대 이런 OLE 표준 문자열을 가리키는 포인터가 바로 그 이름도 유명한 BSTR이다. 모든 BSTR은 (L)PCWSTR과 호환된다. 그러나 PCWSTR은 스택이든 힙이든 아무 메모리나 가리킬 수 있기 때문에 그게 곧 BSTR이라고 간주할 수는 없다. 관계를 알겠는가? BSTR은 SysAllocString 함수를 통해 생성되고 SysFreeString 함수를 통해 해제된다.

'내 문서', '프로그램 파일' 등 운영체제가 특수한 용도로 예정하여 사용하는 디렉터리를 구하는 함수로 SHGetSpecialFolderPath가 있다. 이 함수는 MAX_PATH만치 확보된 메모리 공간을 가리키는 문자 포인터를 입력으로 받았으며, 특수 폴더들을 CSIDL이라고 불리는 일종의 정수값으로 식별했다.

그러나 윈도우 비스타에서 추가된 SHGetKnownFolderPath는 폴더들을 128비트짜리 GUID로 식별하며, 문자열도 아예 포인터의 포인터 형태로 받는다. 21세기에 도입된 API답게, 이 함수가 그냥 메모리를 따로 할당하여 가변 길이의 문자열을 되돌려 준다는 뜻이다. 260자 제한이 없어진 것은 좋지만, 이 함수가 돌려 준 메모리는 사용자가 따로 CoTaskMemFree로 해제를 해 줘야 한다. SysFreeString이 아님. 메모리만 COM 표준 함수로 할당했을 뿐이지, BSTR이 돌아오는 게 아닌 것도 주목할 만한 점이다.

예전에 FormatMessage 함수도 FORMAT_MESSAGE_ALLOCATE_BUFFER 플래그를 주면 자체적으로 메모리가 할당된 문자열의 포인터를 되돌리게 할 수 있는데, 이놈은 윈도우 NT 3.x 시절부터 있었던 함수이다 보니, 받은 포인터를 LocalFree로 해제하게 되어 있다.

이렇게 운영체제 API 차원에서 메모리를 할당하여 만들어 주는 문자열 말고, 프로그래밍 언어가 제공하는 문자열은 메모리 관리에 대한 센스가 추가되어 있다. 대표적인 예로 MFC 라이브러리의 CString이 있다.

CString 자체는 BSTR과 마찬가지로 언뜻 보기에 PCWSTR 포인터 하나만 멤버로 달랑 갖고 있다. 그래서 심지어 printf 같은 문자열 format 함수에다가 "%s", str처럼 개체를 명시적인 형변환 없이 바로 넘겨 줘도 괜찮다(권장되는 프로그래밍 스타일은 못 되지만).

그런데 그 포인터의 앞에 있는 것이 단순히 문자열 길이 말고도 더 있다. 바로 레퍼런스 카운트와 메모리 할당 크기. 그래서 문자열이 단순 대입이나 복사 생성만 될 경우, 그 개체는 동일한 메모리를 가리키면서 레퍼런스 카운트만 올렸다가, 값이 변경되어야 할 때만 실제 값 복사가 일어난다. 이것을 일명 copy-on-modify 테크닉이라고 하는데, MFC 4.0부터 도입되어 오늘날에 이르고 있다. 이는 상당히 똑똑한 정책이기 때문에 이것만 있어도 별도로 r-value 참조자 대입 최적화가 없어도 될 정도이다.

메모리 할당 크기는 문자열에 대해 덧셈 같은 연산을 수행할 때 메모리 재할당이 필요한지를 판단하기 위해 쓰이는 정보이다. MFC는 표준 C 라이브러리에 의존적이기 때문에 이때는 응당 malloc/free가 쓰인다. 재할당 단위는 보통 예전에 비해 배수 단위로 기하급수적으로 더 커진다.

CString이 그냥 포인터와 크기가 같은 반면, 표준 C++ 라이브러리에 존재하는 string 클래스는 비주얼 C++ 2010 x86 기준 개체 하나의 크기가 28바이트나 된다. 길이가 16 이하인 짧은 문자열은 그냥 자체 배열에다 담고, 그보다 긴 문자열을 담을 때만 메모리를 할당하는 테크닉을 쓰기 때문이다. 그리고 대입이나 복사를 할 때마다 CString 같은 reference counting을 하지 않고, 일일이 메모리 재할당과 값 복사를 한다.

글을 맺겠다.
C/C++이 까이는 여러 이유 중 하나는 라이브러리가 지저분하고 동일 기능의 중복 구현이 너무 많아서 혼란스럽다는 점이다. 문자열도 그 범주에 정확하게 속하는 요소일 것이다. 메모리 할당과 해제 자체부터가 구현체 중복이 한둘이 아니니... 어지간히 덩치와 규모가 있는 프레임워크 라이브러리는 그냥 자신만의 문자열 클래스 구현체를 갖고 있는 게 이상한 일이 아니다. 하지만 그건 C/C++이 쓰기 편리한 고급 언어와 시스템 최적화 오덕질이라는 두 토끼를 모두 잡으려다 어쩔 수 없이 그리 된 것도 강하다.

문자열에 대한 이야기 중에서 일부는 내가 예전 블로그 포스트에서도 한 것도 있지만, 이번 글에 처음으로 언급한 내용도 많을 것이다. 프로그래밍 언어 중에는 문자열을 다루기가 기가 막히게 편리한 것이 있는데, 그런 것도 내부적으로는 다 결국은 컴퓨터가 무진장 고생해서 결과물을 만들어 내는 것이다.
컴퓨터가 받아들이고 뱉어내는 문자열들이 내부적으로 어떤 구현체에 의해 어떤 처리를 거치는지를 생각해 보는 것도 프로그래머로서는 의미 있는 일일 것이다.

Posted by 사무엘

2012/10/13 08:26 2012/10/13 08:26
, , , ,
Response
No Trackback , 8 Comments
RSS :
http://moogi.new21.org/tc/rss/response/743

Trackback URL : http://moogi.new21.org/tc/trackback/743

Comments List

  1. 주의사신 2012/10/13 09:40 # M/D Reply Permalink

    1. 조엘 온 소프트웨어를 읽다 보면 저자가 해 본 프로젝트 중에서 CF***edString이라는 자료형이 있었다고 합니다. 얼마나 만드는 사람이 화가 났으면 이름을 그렇게 지었을까 싶은 문자열 클래스입니다.

    2. C++에 이제는 문자열 자료형을 포함하지 않으면 안 되는 이유가 없을 것 같은데, 어떠한 이유인지 전혀 관련된 논의를 하지 않는 것 같더군요.

    3. std::string의 경우에는 modify-on-copy를 하지 않는데, 이렇게 하는 이유가 Multithread 프로그래밍을 하게 되면, string 관련 연산을 할 때 Lock을 자주 걸어 주어야 해서 성능 저하가 엄청나기 때문에 그랬다고 합니다. (Code Craft라는 책에 그렇게 나와 있었습니다.)

    1. 사무엘 2012/10/13 10:10 # M/D Permalink

      1. 하하하!!! 흠좀무스러운 작명이군요.

      2. string은 C++ 라이브러리에 포함된 것으로도 감지덕지지, 개념적으로 customization의 여지가 너무 많아서 C++ 같은 언어의 기본 자료형으로 들어가는 건 이제 무리일 것 같습니다. 그리고 넣어 봤자 이제는 이미 중복 구현체들이 역할을 대신 수행하고 있고요.
      당장 wchar_t만 해도 int만큼이나 크기가 들쭉날쭉.. Windows에서는 UTF16이 짱입니다만, 유닉스 계열로 가면 아예 1글자당 4바이트인 UTF32도 많이 쓰이지요?

      3. std::string이 그렇게 설계된 것은 스레드 안전성 같은 걸 차지하고라도, 문자열을 뭔가 serious한 객체라기보다는 primitive한 약간 덩치 큰 기본 자료형으로 간주한 사상이 크게 작용해서인 것 같습니다.

      아무튼, 문자열이라는 물건은 프로그래머로서 생각할 주제가 굉장히 많은 주제임은 분명합니다. ^^

  2. kippler 2012/10/13 12:51 # M/D Reply Permalink

    * 이러니 저러니 해도 저는 개인적으로 CString 이 제일 좋더군요. 효율성도 좋고, %s 파라메터로 쓸 수 있는것도 좋고, 편리한 메쏘드도 많이 갖춰져 있고..

    * windows 2000 이전은 UCS2 였고, 2000 부터는 공식적으로 UTF-16이라고 하더군요. 사실 그 당시는 아직 유니코드가 UCS2 만 있고, UCS4 는 없던 시절이라....

    * 그리고 utf16 이 유닉스의 ucs4 보다는 확실히 편한듯 합니다. 어차피 ucs2 를 넘어가는 문자열 다룰일이 많지 않고, 메모리도 절약되니깐요.

    * 고급언어만 쓰던 개발자는 문자열 타입을 추상적으로 생각하는 경우가 많더군요. 그래서 replace 같은 함수가 얼마나 cpu를 많이 쓰는지에 대해서는 생각해 본 적이 없는 경우도 많고요. 그래서 신입사원 뽑을때 문자열 관련 함수를 직접 구현해 보게 시키는것(strlen, strcpy..) 이 c 의 로우레벨이나 포인터에 대한 이해도를 측정하기 좋더군요.

    1. 사무엘 2012/10/13 21:25 # M/D Permalink

      앗, 유명하신 그분께서...! (형님, 반갑습니다.. 이렇게 불러 드려도 되죠? ^^)
      저도 문자열 클래스라는 걸 제일 먼저 접한 게 CString이다 보니, 그 디자인이 가장 무난하고 마음에 드는 것 같습니다. 사실, thread safety라는 게 절대적으로 보장돼야 하는 상황은 그리 많지 않거든요. 멀티스레딩을 하는 상위 소프트웨어 계층에서 동기화를 알아서 해 줘야겠죠. 굳이 포인터 단위의 저수준 조작이 필요하면 GetBuffer 함수를 써서 내부 버퍼를 변형 가능한 형태로 잠시 까 볼 수도 있다는 점도 더욱 좋습니다.

      UCS와 UTF 사이의 관계는 말씀하신 대로이구요. 진짜로 UC2 범위를 넘어가는 문자를 일상생활에서 접할 일은 거의 없는 게 사실이죠. 아무리 메모리가 남아 돈다고 해도, 한 글자당 32비트는 솔직히 정서적으로 기억장소 낭비가 너무 심한 감이 있죠.

      또한, 마치 실수가 정수보다 다루기 힘들듯, 문자열은 단순 문자나 숫자에 비해 컴퓨터가 다루기 힘들어하는 타입이라는 것을 프로그래머라면 알 필요가 있습니다. ^^;;

  3. 김 기윤 2012/10/13 13:13 # M/D Reply Permalink

    옛날부터 단순히 char* 또는 char[] 형 만 계속해서 다루다 보니까
    std::string 이나 CString 같은 것을 쓰다가도 문자열 조작을 하려면
    c_str() 같은 것으로 char* 로 변환 한 뒤 변환하고 다시 원래대로 되돌리는 등의 연산을 하는
    삽질(..)을 하는 저입니다.

    std::string 이나 CString 쪽도 익숙해져야 할텐데, 그냥 버릇이 버릇이다보니 기회를 잡지를 못하고 있네요..;

    1. 사무엘 2012/10/13 21:25 # M/D Permalink

      포인터 덕질을 하다가 string 클래스로 갈아타자니, 내가 원하는 조작을 마음대로 못 하는 게 좀 마음에 걸리기도 하지요.
      저의 <날개셋> 한글 입력기도 자체적인 문자열 클래스 구현체가 있는데요, 주어진 구간의 문자열을 특정 문자열로 대체하는 Substitute 함수가 있고(대체 대상 문자열은 굳이 null-terminate가 아니어도 됨!), Format된 문자열을 기존 문자열의 앞이나 뒤에다 추가하는 FormatBefore, FormatAfter라는 함수를 제 식대로 만들어서 제공하고 있답니다.

  4. Lyn 2012/10/13 15:47 # M/D Reply Permalink

    copy-on-modify 보다는 copy-on-write 가 좀더 일반적으로 쓰이는 명칭일듯 합니다

    1. 사무엘 2012/10/13 21:25 # M/D Permalink

      제가 CString 클래스의 메커니즘을 처음으로 알게 된 곳이 바로 MSDN에서 전설적인 개발자였던 Paul DiLascia의 글인데요, 거기서 처음부터 'modify'라는 용어를 쓰더군요. 저는 그냥 그 영향을 받은 것입니다.

      http://www.microsoft.com/msj/archive/S1F0A.aspx
      First off, if you're not using CStrings, shame on you! Come on guys, the year 2000 is almost upon us!

      “세상에 아직도 CString도 모르다니, 부끄러운 줄 아셈!”... 1996년에 작성된 글입니다. ㅎㅎ

Leave a comment

1. C/C++이 빌드가 느린 이유

베테랑 프로그래머라면 이미 다 알기도 하겠지만, C/C++ (특히 C++)은 강력한 대신 정말 만년 굼벵이 언어가 될 수밖에 없는 요인만 일부러 골고루 가진 채 만들어졌다 해도 과언이 아닌 것 같다.

뭐가 굼벵이냐 하면 두 말할 나위도 없이 빌드 속도 말이다. C#, 자바, 델파이 같은 다른 언어나 툴과 비교하면 안습 그 자체. 이건 컴퓨터의 속도만 빨라진다고 해서 극복 가능한 차원의 차이가 아니라 구조적으로 심한 부담과 비효율 때문이다. 이 점에 대해서는 본인도 예전에 여러 글을 블로그에다 언급한 적이 있다.

  • 일단 C++은 태생이 바이트코드 같은 가벼운 가상 기계가 아니라 철저하게 기계어 네이티브 코드 생성 지향이다. 다른 가벼운(?) 언어들과는 위상부터가 다르며, 이 상태에서 최적화까지 가면 부담은 더욱 커진다. 게다가 이 언어는 설계 철학이 컴파일 시점 때 최대한 많은 걸 미리 결정하는 걸 지향하고 있다. 가령, 자바에 inline이라든가 함수 호출 규약, 레지스터, C++ 수준의 static한 템플릿 메타프로그래밍, 혹은 링크 타임 코드 생성 같은 개념이 있지는 않다.
  • 또한 이 언어는 근본적으로 문법이 상당히 문맥 의존적이고 복잡하여, 구문 분석이 어렵다. 단적인 예로 함수 선언과 객체 선언 A b(c); 변수 선언과 단순 연산식 B*c; 형변환 연산과 단순 연산식 (c)+A 가 c가 무엇인지 문맥에 따라 왔다갔다 하면서 완전히 달라진다. 거기에다 C++의 경우 템플릿, 오버로딩, namespace ADL까지 가면 난이도는 정말 안드로메다로. 다른 언어는 O(n log n) 시간 복잡도만으로도 되는 구문 분석 작업이 C++은 반드시 O(n^2)을 쓰지 않으면 안 되는 과정이 있다고 한다.
  • 빌드를 위해 전처리, 링크 같은 복잡한 계층이 존재하며, 특히 링크는 병렬화도 되지 않아 속도를 더욱 올릴 수가 없는 작업이다. 한 모듈에서 참조하는 함수의 몸체가 다른 어느 번역 단위에 있을지는 전혀 알 수 없다!
    그런데 요즘 C++ 컴파일러의 트렌드는 1에서 잠시 언급했듯이 링크 타임 때의 코드 생성과 최적화(인라이닝 포함)여서 이런 병목 지점에서 더욱 많은 작업량이 부과되고 있다. 이런??

이런 특징은 유독 C/C++ 언어만 개발툴/IDE에서 프로젝트를 만들면 온갖 잡다한 보조 데이터 파일들이 많이 생성되는 이유와도 일맥상통한다. 소스 코드를 잽싸게 분석하여 인텔리센스 같은 똑똑한 IDE 기능을 제공하기가 여타 언어들보다 훨씬 더 어렵기 때문이다.

2. 인클루드의 문제점

그런데, 네이티브 코드 생성, 복잡한 문법 같은 것 이상으로 C/C++의 빌드 시간을 더욱 뻥튀기시키고 빌드 작업을 고달프게 하는 근본적인 요소는 전처리 중에서도 다름아닌 #include 남발이다. C/C++은 남이 만들어 놓은 함수, 클래스, 구조체 같은 프로그래밍 요소를 쓰려면 해당 헤더 파일을 무조건 인클루드해 줘야 한다.

일단 이건 문법적으로는 인위적인 요소가 없이 깔끔해서 좋다. 인클루드되는 헤더는 역시 C/C++ 문법대로 작성된 일반 텍스트 파일이며, 내가 짜는 프로그램이 참조하는 명칭들의 출처가 여기 어딘가에는 반드시 있다고 보장됨을 내 눈으로 확인할 수 있다. 그러나 DB 형태로 최적화된 바이너리 파일이 아니라 파싱이 필요한 텍스트 파일이란 점은 일단 빌드 속도의 저하로 이어진다. 이게 문제점 하나.

본격적인 C++ 프로그램을 하나 만들려면 표준 C/C++ 라이브러리뿐만이 아니라 윈도우 API, MFC, 그리고 다른 3rd-party 라이브러리, 게임 엔진 등 갖가지 라이브러리나 프레임워크가 제공하는 헤더 파일을 참조하게 된다. 이것들을 합하면 한 소스 코드를 컴파일하기 위해 인클루드되는 헤더 파일은 가히 수십, 수백만 줄에 달하게 된다.

게다가 이 인클루드질은 전체 빌드를 통틀어 한 번만 하고 끝인 게 아니라, 이론적으로는 매 번역 단위마다 일일이 새로 해 줘야 한다. 헤더 파일 의존도가 개판이 돼 버리는 바람에 헤더 파일 하나 고칠 때마다 수백 개의 cpp 파일이 재컴파일되는 문제는 차라리 애교 수준이다. 문제점 둘.

보통 헤더 파일에는 중복 인클루드 방지를 위한 guard가 있다.

#ifndef ___HEADER_DEFINED_
#define ___HEADER_DEFINED_

/////

#endif

그런데 #if문 조건을 만족하지 못하는 줄들은 단순히 구문 분석과 파싱만 skip될 뿐이지, 컴파일러는 여전히 중복 인클루드된 헤더 파일도 각 줄을 일일이 읽어서 #else나 #endif가 나올 때까지 들여다보긴 해야 한다.

많은 사람들이 간과하는 사실인데(사실 나도 그랬고), 그때는 컴파일 작업만 잠시 중단됐을 뿐, 전처리기는 전체 소스를 대상으로 여전히 동작하고 있다. 중복 인클루드가 컴파일러의 파일 액세스 트래픽을 얼마나 증가시킬지가 상상이 되는가? guard만 있다고 장땡이 아니며, 이게 근본적으로 얼마나 비효율적인 구조인지를 먼저 알아야 한다. 문제점 셋.

그리고 이 #include의 수행 결과를 컴파일러나 IDE로 하여금 예측이나 최적화를 도무지 할 수가 없게 만드는 치명적인 문제는 극단적인 문맥 의존성이다.
헤더 파일은 그저 static한 함수, 클래스, 변수 선언의 집합체가 아니다. 엄연히 C/C++ 소스 코드의 일부를 구성하며, 동일한 헤더라 해도 어떤 #define 심벌이 정의된 상태에서 인클루드하느냐에 따라서 그 여파가 완전히 달라질  수 있다.

극단적인 예로, 한 소스 파일에서 #define 값만 달리하면서 동일 헤더 파일을 여러 번 인클루드함으로써, 템플릿 비스무리한 걸 만들 수도 있단 말이다. 일례로, 비주얼 C++ 2010의 CRT 소스에서 strcpy_s.c와 wcscpy_s.c를 살펴보기 바란다. 베이스 타입만 #define을 통해 char이나 wchar_t로 달리하여 똑같이 tcscpy_s.inl을 인클루드하는 걸로 구현돼 있음을 알 수 있다...;;

물론 인클루드를 실제로 그렇게 변태적으로 활용하는 예는 극소수이겠지만 인클루드는 여타 언어에 있는 비슷한 기능인 import나 use 따위와는 차원이 다른 개념이며, 캐싱을 못 하고 그 문맥에서 매번 일일이 파싱해서 확인해 보는 수밖에 없다. 문제점 넷이다.

이런 문제 때문에 여타 언어들은 텍스트 파싱을 수반하는 인클루드 대신, 별도의 패키지 import 방식을 쓰고 있으며, Objective C도 #include 대신 #import를 제공하고 헤더 파일은 무조건 중복 인클루드가 되지 않는 구조를 채택하여 셋째와 넷째 문제를 피해 갔다.

비주얼 C++도 #pragma once라 하여 #endif를 찾을 것도 없이 중복 인클루드를 방지하고 파일 읽기를 거기서 딱 중단하는 지시자를 추가했다. 이건 비표준 지시자이긴 하지만 전통적인 #ifdef~#endif guard보다 빌드 속도를 향상시키는 테크닉이기 때문에 프로그래머의 입장에서는 사용이 선택이 아닌 필수이다. 물론, 단순히 중복 선언 에러만 방지하는 게 아니라 특정 헤더 파일의 인클루드 여부를 알려면 재래식 #define도 좀 해 줘야겠지만 말이다.

외부에서 기선언된(predefined) 프로그래밍 요소를 끌어오는데, namespace나 package 같은 언어 계층을 거친 명칭이 아니라 생(raw-_-) 파일명의 지정이 필요한 것부터가 오늘날의 관점에서는 꽤 원시적인 작업이다. 개인적으로는 인클루드 파일의 경로를 찾는 메커니즘도 C/C++은 너무 복잡하다고 생각한다.

""로 싸느냐 <>로 싸느냐부터 시작해서, 인클루드가 또 다른 파일을 중첩으로 인클루드할 때, 다른 인클루드 파일을 자기 디렉터리를 기준으로 찾을지 자신을 인클루드 한 부모 파일의 위치로부터 찾을지, 프로젝트 설정에 명시된 경로에서 찾을지 같은 것 말이다…;; 게다가 인클루드 명칭도 #define에 의한 치환까지 가능하다. #include MY_HEADER처럼. 그게 가능하다는 걸 FreeType 라이브러리의 소스를 보면서 처음으로 알았다.

그런데 그러다가 서로 다른 디렉터리에 있는 동명이인 인클루드 파일이 잘못 인클루드되기라도 했다면.. 더 이상의 자세한 설명은 생략. 내가 무심코 선언한 명칭이 어디엔가 #define 매크로로도 지정되어 있어서 엉뚱하게 자꾸 치환되고 컴파일 에러가 나는 것과 같은 악몽이 발생하게 된다! 문제점 다섯.

이것도 어찌 보면 굉장히 문맥 의존적인 절차이기 때문에, 오죽했으면 비주얼 C++ 2010부터는 인클루드/라이브러리 디렉터리 지정을 global 단위로 하는 게 완전히 없어지고 전적으로 프로젝트 단위로만 하게 바뀌었다는 걸 생각해 보자.

C++ 프로젝트에서 MFC의 클래스나 윈도우 API의 함수를 찍고 '선언으로 가기'를 선택하면 afxwin.h라든가 winbase.h 같은 표준 인클루드 파일에 있는 실제 선언 지점이 나온다. 그 방대한 헤더 파일을 매 빌드 때마다 일일이 파싱할 수가 없으니 인텔리센스 DB 파일 같은 건 정말 크고 아름다워진다.

그에 반해 C# 닷넷 프로젝트에서 Form 같은 클래스의 선언을 보면, 컴파일러가 바이너리 수준에서 내장하고 있는 클래스의 껍데기 정보가 소스 코드의 형태로 생성되어 임시 파일로 뜬다…;; 이게 구시대 언어와 신세대 언어의 시스템 인프라의 차이가 아닌가 하는 생각이 들었다.

그래서 이런 C++ 인클루드 체계의 비효율 문제는 어제오늘 제기되어 온 게 아니기 때문에, 컴파일러 제조사도 좀 더 근본적인 문제 회피책을 간구하게 됐다. 그래서 나온 것이 그 이름도 유명한 precompiled 헤더이다. stdio.h나 stdlib.h 정도라면 모를까, 매 번역 단위마다 windows.h나 afx.h를 일일이 인클루드해서 파싱한다는 건 삽질도 그런 삽질도 없으니 말이다..

3. precompiled header의 도입

일단 프로젝트 내에서 "인클루드 전용" 헤더 파일과 이에 해당하는 번역 단위를 설정한다. 비주얼 C++에서 디폴트로 주는 명칭이 바로 stdafx.cpp와 stdafx.h이다. 모든 번역 단위들이 공용으로 사용하는 방대한 양의 프레임워크, 라이브러리의 헤더를 몰아서 인클루드시킨다. 컴파일러 옵션으로는 Create precompiled header에 해당하는 /Yc "stdafx.h"이다.

그러면 그 헤더 뭉치들은 stdafx.cpp를 컴파일할 때 딱 한 번만 실제로 인클루드와 파싱을 거치며, 이 파일들의 분석 결과물은 빠르게 접근 가능한 바이너리 DB 형태인 프로젝트명.pch 형태로 생성된다.

그 뒤 나머지 모든 소스 파일들은 첫 줄에 #include "stdafx.h"를 반드시 해 준 뒤, Use precompiled header인 /Yu "stdafx.h" 옵션으로 컴파일한다. 그러면 이제 stdafx.h의 인클루드는 실제 이 파일을 열어서 파싱하는 게 아니라, 미리 만들어진 PCH 파일의 심벌을 참고하는 것으로 대체된다! 앞에서 제기한 인클루드의 문제점 중 첫째와 둘째를 극복하는 셈이다.

pch 파일이 생성되던 시점의 문맥과 이 파일이 실제로 인클루드되는 시점의 문맥은 싱크가 서로 반드시 맞아야 한다. 그렇기 때문에 소스 코드에도 문맥상의 제약이 걸린다. PCH를 사용한다고 지정해 놓고 실제로 stdafx.h를 맨 먼저 인클루드하지 않은 소스 파일은 Unexpected end of file이라는 컴파일 에러가 발생하게 된다. PCH 개념을 모르는 프로그래머는 C++ 문법에 아무 하자가 없는 외부 소스 코드가 왜 컴파일되지 않는지 이해를 못 할 것이다.

당연한 말이지만 stdafx.h가 인클루드하는 헤더 파일의 내용이 수정되었다면 PCH 파일은 다시 만들어져야 하며, 이때는 사실상 프로젝트 전체가 리빌드된다. 그러므로 stdafx.h 안에는 거의 수정되지 않는 사실상 read-only인 헤더 파일만 들어가야 한다.

인클루드 파일만 수십, 수백만 줄에 달하는 중· 대형 C++ 프로젝트에서 PCH가 없다는 건 상상조차 할 수 없는 일이다. 얼마 되지도 않는(많게 잡아도 200KB 이내) 소스 코드들이 high-end급 컴에서 그것도 네트워크도 아닌 로컬 환경에서 빌드 중인데 소스 파일 하나당 컴파일에 1초 이상씩 잡아 처먹는다면, 이건 인클루드 삽질 때문일 가능성이 매우 높다. 이 경우, 당장 PCH를 사용하도록 프로젝트 설정을 바꾸고 소스 코드를 리팩터링할 것을 심각하게 고려해야 한다. 이건 작업 생산성과 직결된 문제이기 때문이다.

아놔, 이렇게 글을 길게 쓸 생각은 없었는데 너무 길어졌다.
요컨대 C++ 프로그래머라면 자기의 생업 수단인 언어가 이런 구조적인 비효율을 갖고 있다는 걸 인지하고, 상업용 컴파일러 및 개발툴이 이를 극복하기 위해 어떤 대안을 내놓았는지에 대해 관심을 가질 필요가 있다.

자바, C#, D 등 C++의 후대 언어들은 C++과 문법은 비슷할지언정 이 인클루드 체계만은 어떤 형태로든 제각각 다 손을 보고 개량했음을 알 수 있다. 아까도 언급했듯, 하다못해 Objective C도 중복 인클루드 하나만이라도 자기 식으로 정책을 바꿨지 않던가.

한 가지 생각할 점은, C/C++은 태생이 이식성에 목숨을 걸었고, 언어의 구현을 위해 바이너리 레벨에서 뭔가 이래라 저래라 명시하는 것을 극도로 꺼리는 언어라는 점이다. 그래서 대표적으로 C++ 함수 decoration이 알고리즘이 중구난방인 아주 대표적인 영역이며, 함수 calling convension도 여러 규격이 난립해 있고 모듈/패키지 같은 건 존재하지도 않는다. 그런 차원에서, 비록 비효율적이지만 제일 뒤끝 없는 텍스트 #include가 여전히 선호되어 온 건지도 모르겠다.

4. 여타 언어의 인클루드

여담이다만, 본인은 베이직부터 쓰다가 C/C++로 갈아탄 케이스이기 때문에 인클루드라는 걸 처음으로 접한 건 C/C++이 아니라 퀵베이직을 통해서였다.

'$INCLUDE: 'QB.BI'

바로, 도스 API를 호출하는 인터럽트 함수와 관련 구조체가 그 이름도 유명한 저 헤더 파일에 있었기 때문이다.

C/C++에 전처리기가 있는 반면, 베이직이나 파스칼 계열 언어는 개념적으로 그런 전처리기와 비슷한 위상인 조건부 컴파일이나 컴파일 지시자는 주석 안에 메타커맨드의 형태로 들어있곤 했다. 그러나 여타 프로그래밍 요소를 끌어다 오는 명령은 메타커맨드나 전처리기가 아니라, 엄연히 언어 예약어로 제공하는 게 디자인상 더 바람직할 것이다.

그리고 파워베이직은 퀵베이직 스타일의 인클루드 메타커맨드도 있고, 파스칼 스타일의 패키지 지정 명령도 둘 다 갖추고 있었다.

Posted by 사무엘

2012/09/21 19:25 2012/09/21 19:25
,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/735

Trackback URL : http://moogi.new21.org/tc/trackback/735

Comments List

  1. lyn 2012/09/23 21:29 # M/D Reply Permalink

    #pragma once는

    Clang(LLVM), gcc, VC, ICC, C++Builder 모두 지원을 하는 키워드이니 더이상 비표준이라며 피할 이유도 없겠지요 : )

    1. 사무엘 2012/09/24 07:06 # M/D Permalink

      우와, 그 정도면 이미 사실상 표준이나 마찬가지이군요.
      언어 스펙만으로 실용적인 필요가 다 충족되지 못해서 온갖 코딩 응용 테크닉 설명이 많이 필요하고(대표적으로 Effective C++ 시리즈라든가)
      컴파일러의 '싸제' 꼼수가 많이 필요하다는 건
      C++의 디자인이 그만큼 불완전하고 defective하다는 점도 있다는 증거라고 볼 수 있습니다.
      저는 옛날에는 그게 무슨 말인지 이론 설명만으로는 이해를 못 했는데 실제로 최근에 와서야 무슨 말인지 좀 알 것 같더군요. ^^

  2. lyn 2012/09/24 21:29 # M/D Reply Permalink

    반대로 말하면 살아있는 언어라고도 할 수 있겠지요 : )

    Pascal, Java, C#, Obj-c는 사실상 독점하고 있는 기업이 있고
    Ruby, Python, Scala 등은 단일 커뮤니티에서 개발을 하죠

    Ada,Cobol 처럼 사용자가 왕창 줄어서 사어수준이 된 언어도 있구요

    C++은 워낙 많은 회사가 컴파일러를 만들고 워낙 많은 사용자가 존재하다 보니 언어 표준이 현실의 요구를 제때 따라가지 못하는 느낌이랄까요 : )
    물론 요즘 업데이트를 보면 얘들이 언어를 만드는건지 언어디자인툴을 만드는건지 모를정도로 확장성"만" 좋은 기능들을 쑤셔넣고 있지만 ㅡ.ㅡ;

    1. 사무엘 2012/09/25 10:59 # M/D Permalink

      하하, 그러게요. 인기를 못 얻은 죽은 언어라면 언어의 불편한 점을 개선하려는 노력도 없을 것이고 풍부한 추가 라이브러리가 만들어지지도 않을 것이고 그대로 없어지겠죠.;;
      마지막에 말씀하신 것처럼 언어가 그런 식으로 확장되면서 지저분해지는 건 피할 수 없는 귀결입니다. 언어야말로 호환성 때문에 함부로 구조를 확 바꿀 수가 없거든요.

      파이썬 2와 3이 난립하고 있는 걸 생각해 봐도 알 수 있듯, 언어라는 건 처음에 만들어질 때 똑똑한 사람이 설계를 정말 잘 해야 하겠다는 생각이 듭니다. 그래서 프로그래밍 언어는 똑똑한 수학/논리학/전산학 덕후들의 재미있는 놀이터에 속하는 대표적인 분야랍니다.

  3. Lyn 2012/09/25 20:28 # M/D Reply Permalink

    파이썬2가 싸놓은건 언제쯤 다 치우려나 (...)

Leave a comment

C/C++, 자바, C# 비교

전산학의 초창기이던 1950년대 후반엔 프로그래밍 언어의 조상이라 할 수 있는 코볼, 포트란 같은 언어가 고안되었다. 그리고 이때 범용적인 계산 로직의 기술에 비중을 둔 알골(1958)이라는 프로그래밍 언어가 유럽에서 만들어졌는데, 이걸 토대로 훗날 파스칼, C, Ada 등 다양한 언어들이 파생되어 나왔다.

이때가 얼마나 옛날이냐 하면, 셸 정렬(1959), 퀵 정렬(1960) 알고리즘이 학술지를 통해 갓 소개되던 시절이다. 구현체는 당연히 어셈블리어.;; 그리고 알골이 도입한 재귀호출이라는 게 함수형 언어가 아닌 절차형 언어에서는 상당히 참신한 개념으로 간주되고 있었다. 전산학의 역사를 아는 사람이라면, 컴퓨터를 돌리기 위해 프로그래밍 언어가 따로 만들어진 게 아니라, 프로그래밍 언어를 구현하기 위해 컴퓨터가 발명되었다는 걸 알 것이다.

알골 자체는 시대에 비해 언어 스펙이 너무 복잡하고 막연하기까지 하며, 구현체를 만들기가 어려워서 IT 업계에서 실용적으로 쓰이지 못했다. 그러나 후대의 프로그래밍 언어들은 알골의 영향을 상당히 많이 받았으니 알골은 가히 프로그래밍 언어계의 라틴어 같은 존재로 등극했다.

물론, 그로부터 더 시간이 흐른 오늘날은 알골의 후예에 속하는 언어인 C만 해도 이미 라틴어 같은 전설적인 경지이다. 중괄호 블록이라든가 C 스타일의 연산자 표기 같은 관행은 굳이 C++, 자바, C# 급의 언어 말고도, 자바스크립트나 PHP처럼 타입이 엄격하지 않고 로컬이 아닌 웹 지향 언어에도 그런 관행이 존재하니 말이다.

C가 먼저 나온 뒤에 거기에 OOP 속성이 가미되어 C++이라는 명작/괴작 언어가 탄생했다. C가 구조화 프로그래밍을 지원하는 고급 언어에다가 어셈블리어 같은 저급 요소를 잘 절충했다면, C++은 순수 OOP 개념의 구현보다는 역시 OOP 이념을 C 특유의 성능 지향 특성에다가 적당히 절충을 잘 했다. 그래서 C++이 크게 성공할 수 있었다.

잘 알다시피 C/C++은 모듈이나 빌드 구조가 컴파일 지향적이며, 거기에다 링크라는 추가적인 작업을 거쳐서 네이티브(기계어) 실행 파일을 만드는 것에 아주 특화되어 있다.

번역 단위(translation unit)라고 불리는 개개의 소스들은 프로그래밍에 필요한 모든 명칭들을 텍스트 형태의 다른 헤더 파일로부터 매번 include하여 참조한 뒤, 컴파일되어 obj 파일로 바뀐다.
한 번역 단위에서 참조하는 외부 함수의 실제 몸체는 어느 번역 단위에 있을지 알 수 없다. 어차피 링크 때 링커가 모든 obj 파일들을 일일이 뒤지면서 말 그대로 연결을 하게 된다.

이 링크를 통해 드디어 실행 파일이나 라이브러리 파일이 최종적으로 만들어진다. 실행 파일은 대상 운영체제가 인식하는 실행 파일 포맷을 따라 만들어지지만 static 라이브러리는 그저 obj들의 모음집일 뿐이기 때문에 lib 파일과 obj 파일은 완전히 같지는 않아도 내부 구조가 크게 차이가 나지 않는다.

이런 일련의 컴파일-링크 계층이 C/C++을 로컬 환경에서의 매우 강력한 고성능 언어로 만들어 주는 면모가 분명 있다. 또한 197, 80년대에는 컴퓨터 자원의 한계 때문에 원천적으로 언어를 그런 식으로 설계해야 하기도 했다.
그러나 오늘날은 대형 프로젝트를 진행할 때 C/C++의 그런 디자인은 심각한 비효율을 초래하기도 한다. 내가 늘 지적하듯이 C보다도 특히 C++은 안습할 정도로 빌드가 너무 느리고 생산성이 떨어진다.

그에 반해 자바는 문법만 살짝 비슷할 뿐 디자인 철학은 C++과는 완전히 다른 언어이다. 잘 알다시피 자바는 의도하는 동작 환경 자체가 native 기계어가 아니라 플랫폼 독립적인 자바 가상 기계이다. 컴퓨터 환경이 발달하고 웹 프로그래밍이 차지하는 비중이 커진 덕분에 이런 발상이 나올 수가 있었던 셈이다.

모든 자바 프로그램은 무조건 1코드, 1클래스이며(단, 클래스 내부에 또 다른 클래스들이 여럿 있을 수는 물론 있음), 심지어 소스 파일 이름과 클래스 이름이 반드시 일치해야 한다. 클래스가 곧 C/C++의 ‘번역 단위’와 강제로 대응한다. 그리고 컴파일된 자바 소스는 곧장 컴파일된 바이트코드로 바뀌며, 이것이 자바 VM이 있는 곳이라면 어디서나 돌아가는 실행 파일(EXE)도 되고 라이브러리(DLL, OBJ)도 된다. 물론, 여러 라이브러리들의 집합체인 JAR이라는 포맷도 따로 있기도 하고 말이다.

클래스 내부에 public static void main 메소드(멤버 함수)만 구현되어 있으면 곧장 실행 가능하다. C++은 C와의 호환을 위해 시작 함수가 클래스 없는 일반 main으로 동일하게 지정돼 있는 반면, 자바는 global scope이 존재하지 않고 모든 명칭이 클래스에 반드시 소속돼 있어야 하기 때문에 그렇다. javac 명령으로 소스 코드(*.java)를 컴파일한 뒤, java 명령으로 컴파일된 바이트코드(*.class)를 실행하면 된다.

다른 모듈을 끌어다 쓸 때도 import로 바이너리 파일을 곧장 지정하면 되니, 텍스트 파싱이 필요한 C++의 #include보다 효율적이다. 번거롭게 *.h와 *.lib (그리고 심지어 *.dll까지)를 일일이 따로 구비할 필요 없다.

요컨대 자바는 C++에 비해 굉장히 많은 자유도와 성능을 제약한 대신, C++보다 훨씬 더 손이 덜 가도 되고 빌드도 훨씬 빨리 되고 프로젝트 세팅도 월등히 더 간편하게 되게 만들어졌다. 함수 호출 규약, 인라이닝 방식, C++ symbol decoration, 링크 에러, CRT의 링크 방식, link-time 코드 생성 최적화 같은 온갖 골치 아프고 복잡한 개념들이 자바에는 전혀 존재하지 않는다.
C++이 벙커에 시즈 탱크에 터렛과 마인 등, 손이 많이 가는 테란이라면, 자바는 프로토스 정도는 되는 것 같다.

자바는 하위 호환성을 고려하지 않은 새로운 언어를 만든 덕분에 디자인상 깔끔한 것도 있지만, 상상도 못 할 편리함을 실현하기 위해 성능도 C++ 사고방식으로는 상상도 못 할 정도로 많이 희생한 것 역시 사실이다. 이는 단순히 메모리 garbage collector가 존재하는 오버헤드 이상이다.

그래서 요즘은 자바 바이트코드를 언어 VM이 그때 그때 실시간으로 네이티브 코드로 재컴파일하여, 자바로도 조금이라도 더 빠른 속도를 내게 하는 JIT(just in time)기술이 개발되어 있다. 비록 이 역시 한계가 있을 수밖에 없겠지만 한편으로는 구조적으로 유리한 점도 있다.

컴파일 때 모든 것이 결정되어 버리는 C++ 기반 EXE/DLL은 사용자의 다양한 실행 환경을 예측할 수 없으니 보수적인 기준으로 빌드되어야 한다. 그러나 자바 프로그램의 경우, VM만 그때 그때 최신으로 업데이트하여 최신 CPU의 명령이나 병렬화 테크닉을 쓰게 하면 그 혜택을 모든 자바 프로그램이 자동으로 보게 된다. 물론 C++로 치면 cout이 C의 printf보다 코드 크기가 작아지는 경지에 다다를 정도로 컴파일러가 똑똑해져야겠지만 말이다.

자바 얘기가 길어졌는데, 다음으로 C#에 대해서 좀 살펴보기로 하자.
C# 역시 네이티브 코드 지향이 아니라 닷넷 프레임워크에서 돌아가는 바이트코드 기반인 점, 복잡한 링크 메커니즘을 생략하고 C++의 지나치게 복잡한 문법과 모듈 구조를 간소화시켰다는 점에서는 자바와 문제 접근 방식이 같다.

단적인 예로, 클래스를 선언하면서 멤버 함수까지 클래스 내부에다 정의를 반드시 집어넣게 한 것, 그리고 생성자 함수의 호출이 수반되는 개체의 생성은 반드시 new를 통해서만 가능하게 한 것은 컴파일러와 링커가 동작하기 상당히 편하게 만든 조치이다. 이는 자바와 C#에 공통적으로 적용된다.

다만 C#은 자바처럼 엄격한 1소스 1클래스 체계는 아니며, 빌드 결과물로 엄연히 일반적인(=윈도우 운영체제가 사용하는 PE 포맷 기반인) EXE와 DLL이 생성된다. 물론 내부엔 기계어 코드가 아닌 바이트코드가 들어있지만 말이다.

C# 역시 클래스 내부에 존재하는 static void Main가 EXE의 진입점(entry point)이 된다. 그러나 C#은 자바 같은 1소스, 1클래스, 1모듈 구조가 아니기 때문에 여러 클래스에 동일한 static void Main이 존재하면 컴파일러가 어느 것을 진입점으로 지정해야 할지 판단할 수 없어서 컴파일 에러를 일으킨다. 링크나 런타임 에러가 아님. 진입점을 별도의 컴파일러 옵션으로 따로 지정해 주거나, Main 함수를 하나만 남겨야 한다.

여담이지만, C#의 진입점 함수는 자바와는 달리 첫 글자 M이 대문자이다. 전통적으로 자바는 첫 글자를 소문자로 써서 setValue 같은 식으로 메소드 이름을 지어 온 반면, 윈도우 세계는 그렇지 않기 때문이다(SetValue).
그리고 C#의 Main은 굳이 public 속성이 아니어도 된다. 어차피 진입점인데 접근 권한이 무엇이면 어떻냐는 식의 발상인 것 같다.

닷넷 실행 파일이 사용하는 바이트코드는 자바와 마찬가지로 기계 독립적인 구조이다. 그러나 그것의 컨테이너라 할 수 있는 윈도우 운영체제의 실행 파일 포맷(PE)은 여전히 CPU의 종류를 명시하는 필드가 존재한다. 그리고 32비트와 64비트에서 필드의 크기가 달라지는 것도 있다. 이것은 기계 독립성을 추구하는 닷넷의 이념과는 어울리지 않는 구조이다. 그렇다면 닷넷은 이런 상황을 어떻게 대처하고 있을까?

내가 테스트를 해 본 바로는 플랫폼을 ‘Any CPU’라고 지정하면, 해당 C# 프로그램은 명목상 그냥 가장 무난하고 만만한 x86 껍데기로 빌드되는 듯하다.
작정하고 x64 플랫폼을 지정하고 빌드하면 헤더에 x64 CPU가 명시된다. 뒤에 이어지는 바이트코드는 어느 CPU에서나 동일하게 생성됨에도 불구하고, 그 프로그램은 x86에서는 실행이 거부되고 돌아가지 않게 된다.

그러니, 64비트 네이티브 DLL의 코드와 연동해서 개발되는 프로그램이기라도 하지 않은 이상, C# 프로그램을 굳이 x64용으로 제한해서 개발할 필요는 없을 것이다. 다만, x86용 닷넷 바이너리는 관례적으로 닷넷 런타임인 mscoree.dll에 대한 의존도가 추가되는 반면 x64용 닷넷 바이너리는 그런 게 붙어 있지 않다. 내 짧은 생각으론, 64비트 바이너리는 32비트에서 호환성 차원에서 넣어 줘야 했던 잉여 사항을 생략한 게 아닌가 싶다.

DLL에 기계 종류와 무관한 리소스나 데이터가 들어가는 일은 옛날부터 있어 왔지만, 닷넷은 코드조차도 기계 종류와 무관한 독립된 녀석이 들어가는 걸 가능하게 했으니 이건 참 큰 변화가 아닐 수 없다. 네이티브 쪽과는 달리 골치 아프게 32비트와 64비트를 일일이 신경 쓸 필요가 없고, 한 코드만으로 x86(-64) 계열과 ARM까지 다 커버가 가능하다면, 정말 어지간히 하드코어한 분야가 아니라면, 월등한 생산성까지 갖추고 있는 C#/자바 같은 개발 환경이 뜨지 않을 수 없을 것 같다. C++과 자바, C#을 차례로 비교해 보니 그런 생각이 들었다.

Posted by 사무엘

2012/06/16 19:37 2012/06/16 19:37
, , ,
Response
No Trackback , 7 Comments
RSS :
http://moogi.new21.org/tc/rss/response/696

Trackback URL : http://moogi.new21.org/tc/trackback/696

Comments List

  1. 주의사신 2012/06/17 15:30 # M/D Reply Permalink

    1. Java도 .NET의 언어 독립성을 조금 따라해 보려고 하고 있더군요.

    2. .NET의 언어는 독립이 되어 있다. 보니 NetCobol이라고 .NET 프레임워크 상에서 작동하는 COBOL도 존재합니다.

    http://www.netcobol.com/cobol-compiler-for-net/

    Visual Studio 2010에서 COBOL이 돌아가는 아름다움이란...

    3. 개인적으로는 Java보다 C#을 더 좋아합니다. Java는 너무 제약 사항이 많더라고요.

    1. 사무엘 2012/06/17 23:39 # M/D Permalink

      1. Free your legacy code from the mainframe with NetCOBOL for .NET.
      닷넷과 코볼의 만남이라..;;; 정말 충격적이군요. 하긴, 비주얼 스튜디오건, 이클립스건 IDE들은 그야말로 완전 오픈 아키텍처로 개발되기 때문에 온갖 언어 확장이 나올 수도 있겠습니다.

      2. 흠 그럼 자바 런타임에다가는 자바 말고 또 무슨 언어가 얹히려나 궁금해지네요.

      3. 아무래도 언어 디자인은 좀 더 늦게 나온 C#이 더 좋을 수밖에 없으며, 저 역시 그 점에서 공감합니다. ^^

    2. Lyn 2012/06/17 23:54 # M/D Permalink

      사무엘님 //

      이미 자바 런타임에서 돌아가는 Scala가 있습니다. 트위터가 메세징시스템을 Ruby 에서 Scala로 갈아탄걸로 유명하죠...
      Play 라는 Framework 도 있구요.

      뭐 Jython이나 JRuby같은 놈들도 있고 ..

      닷넷이야 뭐 원체 언어가 많으니 ㅡㅡㅋ;
      상용이라면 Delphi Prism 이 있겠네요.

  2. 김 기윤 2012/06/18 11:51 # M/D Reply Permalink

    C# 과 Java 양쪽 모두 제대로 사용하면 C++ 보다 생산성이 높다는 것은 저도 인지하고 있지만, 여태껏 C++ 만 해왔고 새로C#과 Java 만의 각종 라이브러리를 익히기 귀찮(-_-;)다는 이유로 계속해서 C++ 위주로 사용중이었습니다.


    ......다만 이제 안드로이드 앱에 손을 대려고 보니, 안드로이드 앱은 Java 기반이라서 강제(?)로 Java 로 갈아타서 개발해 보고 있는데, 여러가지로 편한게 많더군요. 언어 자체는 C++ 을 제약시킨 느낌이라서 어려운 것도 없구요.

    1. 사무엘 2012/06/18 14:05 # M/D Permalink

      C#은 잘만 다룰 줄 알면 인생을 아주 편하게 해 줄 언어임은 틀림없습니다만,
      저 역시, 말씀하신 것과 동일한 귀차니즘 때문에 C++ 신봉자입니다.
      더구나 제 밥줄인 프로젝트들은 모두 잘 알다시피 닷넷 같은 것과는 관계없는 네이티브 관할이어서 말이죠.

      저는 차라리 맥OS의 데스크톱 환경에 관심이 있어서 Xcode와 옵C를 좀 살펴봤는데 상상 이상의 이질감과 진입 장벽에 좌절하고 있는 중.

  3. GplWorker 2012/06/29 13:56 # M/D Reply Permalink

    고수의 필이 물씬 풍기는 곳을 찾게 되어 반갑습니다.
    좋은 글들 앞으로 차근 차근 읽고 배워가겠습니다.
    Java, Android의 숲속에서 요즘 일광욕을 즐기고 있습니다.
    감사합니다.

    1. 사무엘 2012/06/29 16:48 # M/D Permalink

      겨우 이 정도 수준으로 고수 얘기를 들으니 쑥스럽습니다. 당장 여기 정기적으로 들르는 분들 중에도 저보다 더 고수가 많을 건데요. ^^
      이곳 글들을 유용하게 읽고 계신다니 감사드리며, 앞으로도 관심 부탁드립니다.

Leave a comment

C++11의 람다 함수

프로그래밍을 하다 보면, 어떤 컨테이너 자료구조의 내부에 있는 모든 원소들을 순회하면서 각 원소에 대해 뭔가 동일한 처리를 하고 싶은 때가 빈번히 발생한다.
그 절차를 추상화하기 위해 C++ 라이브러리에는 algorithm이라는 헤더에 for_each라는 템플릿 함수가 있다. 다음은 이 함수의 로직을 나타낸 C++ 코드이다. 딱히 로직이라 할 것도 없이 아주 직관적이고 간단하다.

template<typename T, typename F>
void For_Each_Counterfeit(T a, T b, F& c)
{
    for(T i=a; i!=b; ++i) c(*i);
}

C++은 템플릿과 연산자 오버로딩을 통해 자료구조에 대한 상당 수준의 추상화를 달성했다.
iterator에 해당하는 a와 b는 그렇다 치더라도, 여기서 핵심은 c이다.
F가 무엇인지는 모르겠지만, 어쨌든 c는 함수 호출 연산자 ()를 적용할 수가 있는 대상이어야 한다.
그럼 무엇이 가능할까? 여러 후보들이 있다.

일단 C언어라면 함수 포인터가 떠오를 것이다. 함수 포인터는 코드를 추상화하는 데 지금까지 고전적으로 쓰여 온 기법이다.

void foo(char p);

char t[]="Hello, world!";
For_Each_Counterfeit(t, t+strlen(t), foo);

C++에서는 클래스가 존재하는 덕분에 더 다양한 카드가 생겼다. 클래스가 자체적으로 함수 호출을 흉내 내는 연산자를 갖출 수 있기 때문이다.

class MyObject {
public:
    void operator()(char x);
};

For_Each_Counterfeit(t, t+strlen(t), MyObject());

그리고 더욱 기괴한 경우이지만, 클래스 자신이 함수의 포인터로 형변환이 가능해도 된다.

class MyObject {
public:
    typedef void (*FUNC)(char);
    operator FUNC();
};

For_Each_Counterfeit(t, t+strlen(t), MyObject());

C++ 라이브러리에는 functor 등 다양한 개념들이 존재하지만, 그 밑바닥은 결국은 C++ 언어의 이런 특성들을 사용해서 구현되어 있다.
여기서 재미있는 점이 있다. 다른 자료형과는 달리 함수 포인터로 형변환하는 연산자 오버로드 함수는, 자신이 가리키는 함수의 prototype을 typedef로 미리 만들어 놓고 반드시 typedef된 명칭으로 선언되어야 한다는 제약이 있다. 이것은 C++ 표준에도 공식적으로 명시되어 있는 제약이라 한다.

이런 어정쩡한 제약이 존재하는 이유는 아마도 함수 선언문에다가 다른 함수를 선언하는 문법까지 덧붙이려다 보니, 토큰의 나열이 너무 지저분해지고 컴파일러를 만들기도 힘들어서인 것 같다. 이 부분에서는 아마 C++ 위원회에서도 꽤 고민을 하지 않았을까.
안 그랬으면 형변환 연산자 함수의 prototype은 아래와 비슷한 괴상한 모양이 됐을 것이다. 실제로 이 함수의 full name을 undecorate한 결과는 이것처럼 나온다.

    operator void (*)(char)();

비주얼 C++에서는 함수를 저렇게 선언하면 그냥 * 부분에서 '문법 에러'라는 불친절한 말만 반복할 뿐이지만, xcode에 기본 내장되어 있는 최신형 llvm 컴파일러는 놀랍게도 나의 의도를 간파하더이다. “함수 포인터로 형변환하려면 반드시 typedef를 써야 합니다”라는 권고를 딱 하는 걸 보고 적지 않게 놀랐다. 이런 차이도 맥북을 안 쓰고 오로지 비주얼 C++ 안에서만 틀어박혀 지냈다면 경험하기 쉽지 않았을 것이다. 우왕~

() 연산자 오버로딩은 this 포인터가 존재하는 C++ 클래스 멤버 함수이며 static 형태가 있을 수 없다.
그러나 함수 포인터로의 형변환 연산자 오버로딩은 this가 없으며 C 스타일의 static 함수와 같은 위상이라는 차이가 존재한다.
두 오버로딩이 모두 존재하면 어떻게 될까? 혹시 모호성 오류라도 나는 걸까?

그런 개체에 함수 호출 ()가 적용되는 경우, () 연산자가 먼저 선택되며, 그게 없을 때에 한해서 함수 포인터 형변환이 차선책으로 선택된다. 모호성 오류가 나지는 않는다.
포인터 형변환 연산자와 [] 연산자가 같이 있을 때 개체에 배열 첨자 참조 []가 적용되는 경우, 역시 [] 가 먼저 선택되고 그게 없을 때 포인터 형변환이 차선으로 선택되는 것과 비슷한 맥락이라 볼 수 있다.

그래서 클래스와 연산자 오버로딩 덕분에 저런 문법이 가능해졌는데, C++11에서는 그걸로도 모자라 또 새로운 문법이 추가되었다. 이른바 람다 함수.

For_Each_Counterfeit(t, t+strlen(t), [](char x) { /* TODO: add your code here */ } );

람다 함수는 코드가 들어가야 할 곳에 함수나 클래스의 작명 따위를 신경쓰지 않고 코드 자체만을 직관적으로 곧장 집어넣기 위해 고안되었다. 세상에 C++에서 OCaml 같은 데서나 볼 수 있을 법한 개념이 들어가는 날이 오다니, 신기하지 않은가?

덕분에 C++은 C언어 같은 저수준 하드웨어 지향성에다가 성능과 이념을 적당히 절충한 수준의 객체지향을 가미했고, 90년대 중반에는 템플릿 메타프로그래밍 개념을 집어넣더니, 이제는 함수형 언어의 개념까지 맛보기로 도입한 가히 멀티 패러다임 짬뽕 언어가 되었다.

함수를 값처럼 표현하기 위해서 lambda 같은 예약어가 별도로 추가된 게 아니다. C/C++은 태생상 예약어를 함부로 추가하는 걸 별로 안 좋아하는 언어이다. (그 대신 문법에 혼동이 생기지 않는 한도 내에서 기호 짬뽕을 좋아하며 그래서 사람이나 컴파일러나 코드를 파싱하는 난이도도 덩달아 상승-_-) 보아하니 타입을 선언하는 부분에서는 배열 첨자가 먼저 오는 일이 결코 없기 때문에 []를 람다 함수 선언부로 사용했다.

람다 함수는 다른 변수에 대입되어서 두고두고 재활용이 가능하다. 그래서 C/C++에서는 전통적으로 가능하지 않은 걸로 여겨지는 함수 내부에서의 함수 중첩 선언을 이걸로 대체할 수 있다.

어떤 함수 안에서 특정 코드가 반복적으로 쓰이긴 하지만 별도의 함수로 떼어내기는 싫을 때가 있다. 굳이 함수 호출 오버헤드 때문이 아니더라도, 해당 코드가 그 함수 내부의 지역변수를 많이 쓰기 때문에 그걸 일일이 함수의 매개변수로 떼어내기가 귀찮아서 그런 것일 수도 있다.
이때 흔히 사용되는 방법은 그냥 #define 매크로 함수밖에 없었는데 이때도 람다 함수가 더 깔끔하고 좋은 해결책이 될 수 있다. 람다 함수는 선언할 때 캡처라 하여 주변의 다른 변수들을 참조하는 메커니즘도 언어 차원에서 제공하기 때문이다.

그렇다면 의문이 생긴다.
람다 함수는 그럼 완전히 새로운 type인가?
기존 C/C++에 존재하는 함수 포인터와는 어떤 관계일까?
정답부터 말하자면 이렇다. 람다 함수는 비록 어쩌다 보니 () 연산을 받아 주고 함수 포인터가 하는 일과 비슷한 일을 하게 됐지만, 활용 형태는 함수 포인터하고 아무 관련이 없으며 그보다 더 상위 계층의 개념이다.

템플릿과 연동해서 쓰인다는 점에서 알 수 있듯, 람다 함수는 함수 포인터와는 달리 calling convension (_stdcall, _cdecl, _pascal 나부랭이 기억하시는가?)이고 리턴값이 나발이고간에 아무 상관이 없다. 그저 코드상에서 함수를 값처럼 다루는 걸 돕기 위해 존재하는 추상적인 개념일 뿐이다. 뭔가 새로운 type이 아니기 때문에 람다 함수를 변수에다 지정할 때는 auto만을 쓸 수 있다. 즉, 다른 자료형이 아닌 람다에 대해서는 auto가 선택이 아니라 필수라는 뜻이다.

auto square=[](int x) { return x*x; };
int n = square(9); //81

square는 템플릿 같은 함수가 아니다. 이 함수의 리턴값은 x*x로부터 자동으로 int라고 유추되었을 뿐이다. [](int x) -> int 라고 명시적으로 리턴 타입을 지정해 줄 수도 있다. 구조체 포인터의 멤버 참조 연산자이던 -> 가 여기서 또 화려하게 변신을 한 셈임. 우와!

또한, sizeof(square)을 한다고 해서 포인터의 크기가 나오는 게 아니다. 사실, 람다 함수에다가 sizeof를 하는 건 void에다가 sizeof를 하는 것만큼이나 에러가 나와야 정상이 아닌가 싶다. 그런 개념하고는 아무 관계가 없기 때문이다.

람다는 함수 포인터가 아니기 때문에, square에다가 자신과 프로토타입이 같은 다른 람다 함수를 대입할 수 있는 건 아니다. 함수 포인터의 역할과 개념을 대체할 뿐, 그 직접적인 디테일한 기능을 대체하지는 못한다. 그렇기 때문에 콜백 함수를 받는 문맥에서

qsort(n, arrsize, sizeof(int), [](const void *a, const void *b) { return *((int*)a) - *((int*)b); } );

구닥다리 C 함수에다가 최신 C++11 문법이라니, 내가 생각해도 정말 변태 같은 극단적인 예이다만,
이런 식으로 람다 함수를 집어넣을 수도 없다.

요컨대 람다 함수는 코드의 추상화에 도움을 주고 종전의 함수 포인터 내지 #define, 콜백 함수 등의 역할을 대체할 수 있는 획기적인 개념이다. C++ 철학대로 디자인된 여타 C++ 라이브러리와 함께 사용하면 굉장한 활용 가능성이 있다. 그러나 이것은 함수 포인터에 대한 syntatic sugar는 절대 아니라는 걸 유의하면 되겠다.

Posted by 사무엘

2012/05/24 08:38 2012/05/24 08:38
, , ,
Response
No Trackback , 15 Comments
RSS :
http://moogi.new21.org/tc/rss/response/686

Trackback URL : http://moogi.new21.org/tc/trackback/686

Comments List

  1. 주의사신 2012/05/24 09:22 # M/D Reply Permalink

    Closure와 변수 캡처에 대한 내용은 들어가지 않았군요...

    Closure를 사용하면, 지역 변수를 함수 내의 전역 변수처럼 사용할 수 있다는....

    Closure 처음 보면 진짜 이상합니다. 그런데 알고 나면 무척 편합니다.

    1. 사무엘 2012/05/24 10:21 # M/D Permalink

      네, 본문에서는 그런 게 있다는 것만 언급하고 그것까지 구체적인 예를 들지는 (귀찮아서-_-) 않았습니다.
      그렇게 자신 바깥의 context까지 끌어들이는 개념이 있기 때문에 람다 함수는 더욱 함수 포인터와 동등한 개념이 될 수 없고 그걸 초월하여 더 상위에 있는 개념이 되겠죠.
      어떤 함수 안에서 또 반복되는 코드 블록을 간추릴 필요가 있을 때 #define의 역할을 잘 대체할 수 있습니다.

  2. 김 기윤 2012/05/24 10:10 # M/D Reply Permalink

    람다는 기존에는 없었던 생소한 개념이지만, 익숙해지면 정말 편하죠.

    어떠한 함수 안에서 이 함수 안에서만 쓰이는 것이라면 람다로 즉석 선언 ㄳ

    PPL 라이브러리나 STL과의 결합도도 ㄳ

    아, 그리고 람다에 대해서는 auto 가 필수가 아닙니다.

    #include <functional>
    으로 functional 을 인클루드 한 뒤에,
    std::function<리턴형(인수1, 인수2, 인수3)>
    형식으로 선언하면, 같은 형식의 람다함수를 저장할 수 있습니다.(당연히 using namespace std; 했다면, std:: 는 생략가능)

    예를 들어,
    std::function<void(int, int, Player&, Player&, int, int)> command;
    이런 식으로 정의를 했다면,
    command = [](int self, int target, Player& player, Player& foes, int currentAbilityIndex, int turn) { /* ... */ }
    는 식으로 대입이 가능합니다.
    저는 이런 식으로 클래스 멤버변수로 람다함수를 가지는 코드도 만든 전적이 있습니다. (전략 패턴으로 쓰기 위해서 ㄳ. 특징상 전략 패턴에 쓰기에도 딱 맞더군요)

    따라서 이런 응용도 가능합니다.

    typedef function<pair<int, int>(pair<int, int>)> func;
    func modifier[3] =
    {
    [] (pair<int, int> p) -> pair<int, int> {
    // 가로, 세로 유지
    return p;
    },
    [] (pair<int, int> p) -> pair<int, int> {
    // 가로, 세로 반전
    return make_pair<int, int>(p.second, p.first);
    },
    [] (pair<int, int> p) -> pair<int, int> {
    // "가로, 세로" 를 "섹션별, 섹션내" 로
    return make_pair<int, int>(p.first / 3 * 3 + p.second / 3, p.first % 3 * 3 + p.second % 3);
    }
    };
    for(int f = 0; f < 3; f++)
    {
    // ..
    pair<int, int> point = modifier(i, j);
    // ..
    }


    잘 생각해보니 다른 응용으로는 람다 함수를 뱉어내는 팩토리도 만들 수 있을 듯 합니다 ㅎㄷㄷ

    1. 사무엘 2012/05/24 10:21 # M/D Permalink

      보충 설명에 감사합니다.
      람다가 auto 말고 다른 곳에 대입 가능하다는 건 완전 처음 듣네요...!
      리턴형(인수1, 인수2, 인수3) ... 이건 템플릿 인자로만 지정 가능한 타입이고 전적으로 람다를 위해서 C++11에서 새로 추가된 문법인 거겠죠?
      C++ 라이브러리도 다 C++ 문법 위에서 만들어진 코드일 테니 저는 언어적인 근간을 더 먼저 따져 보는 걸 좋아합니다.

    2. 김 기윤 2012/05/24 10:37 # M/D Permalink

      MSDN에 찾아보니(..어쩌다가 사용법만 알고 MSDN은 지금 처음 가봤습니다 ㄳ) 딱히 람다만들 위한 건 아닌 것 같습니다.
      http://msdn.microsoft.com/ko-kr/library/bb982519

      리턴형(인수1, 인수2, 인수3) 은 저도 여기에서 처음 본 타입이기에 저도 생소한데, 더 알아봐야겠습니다.

    3. Lyn 2012/05/24 13:59 # M/D Permalink

      http://www.devpia.com/Maeul/Contents/Detail.aspx?BoardID=51&MAEULNO=20&no=8580&page=1

      사무엘님 제가 얼마전 올린 글입니다. 참고삼아 보셔도 괜찬을것 같습니다.

      C++11에 새로 추가된 문법은 아니고 boost에서 개발하여 c++11에 정식으로 추가된 범용 함수객체와 바인더, 플레이스홀더 라이브러리 입니다.
      문법이 아니라 템플릿의 타입추론을 이용하여 라이브러리 레벨에서 구현된겁니다.
      C++11을 지원하는 컴파일러엔 std::function 으로 사용 가능하고 그 이전의 컴파일러라면 boost 설치 후 boost::function 으로 사용 가능합니다

  3. Lyn 2012/05/24 14:00 # M/D Reply Permalink

    그런데 왜 내가 쓴 주소는 링크가 안걸리는거야 Orz

  4. 사무엘 2012/05/25 14:47 # M/D Reply Permalink

    흐.. 덕분에 난생 처음 보는 C++ 문법을 새로 알게 됐군요. C++의 세계는 너무 오묘합니다.

    template<typename T>
    class MyObject {
    public:
    T m_dat;
    };

    이런 템플릿 클래스에다가 타입을 아래와 같이 명시해 주면 m_dat는 멤버 변수가 아니고 함수 포인터도 아닌 정식 멤버 함수가 됩니다. 이 문법 자체는 C++11 이전부터 존재해 온 듯합니다.

    MyObject<int(int)> mp;
    int n=mp.m_dat(1);

    MyObject<void(double, double)> mq;
    mq.m_dat(1.0, 2.0);

    “타입( [타입 [, 타입]] )”과 같은 타입 지정자는 여타 타입 지정과는 달리, 템플릿 인자로만 줄 수 있는 문법이겠지요.
    그렇게 해 준 뒤에는 사용되는 타입별로 m_dat 함수를 따로 구현해 줘야 합니다.

    template<>
    int MyObject<int(int)>::m_dat(int x)
    { return x*x; }

    template<>
    void MyObject<void(double, double)>::m_dat(double x, double y)
    { return x*y; }

    결국 템플릿 인자와 함수 자체의 인자에 동일한 함수 프로토타입이 두 번 명시되는 꼴이군요.
    이걸 안 하고서 m_dat 호출만 하면 코드가 컴파일은 되지만 결국 링크 에러가 납니다.

    템플릿 함수라도 타입이 명확하게 지정되어 있는 특화된 몸체는 다른 번역 단위(소스 코드)에 있어도 괜찮습니다.
    그에 반해 typename이라는 것만 명시되어 있는 추상적인 몸체는, 사용되는 모든 번역 단위에서 일일이 인클루드되어야 사용 가능합니다.

    그것도 외부 번역 단위에서 알아서 끌어다 쓸 수 있게 하려고 제안되었던 기능이 export 키워드인데 완전히 흑역사로 전락했고, C++11에서는 보류 정도가 아니라 삭제되어 버린 것 아시죠?
    단순히 변수 타입 정도가 아니라 멤버 함수와 람다까지 왔다갔다 할 수 있는 템플릿은 그 유연함이 가히 #define에 맞먹는 수준인데, 그 모든 가능성을 그것도 기계어 코드 컴파일 언어에서 export 가능하려면 컴파일러와 링커를 다 새로 만들지 않고서는 불가능할 겁니다.

    아무리 생각해도 T가 멤버 함수까지 가리킬 수 있는 건 좀 흉악(?)한 것 같습니다.
    typename이라는 키워드가 있는 것처럼 funcname 같은 다른 힌트 지정자라도 있는 게 낫지 않을까 싶기도 하군요.
    또한, C++ 멤버 함수와 람다 함수는 직접적으로 관계가 있는 게 아닌데 std::function 클래스는 아무리 템플릿이라 해도 어떻게 람다와 곧장 직결이 가능한지 모르겠습니다.

    함수 호출 규약이나 기계어 메모리 구조와 관련된 트릭을 쓰기라도 한 건지, 라이브러리 소스를 봐서는 모르겠고요. (온갖 외계어 같은 #define과 typedef 꼼수들.. -_-)
    그 기능만을 위한 문법이 따로 존재하는 게 아니라면 어떤 형태로든 꼼수가 안 들어갈 수는 없을 텐데요.

    그리고 xcode에서는 MyObject<int(int)>와 같은 문법이 컴파일이 안 되더군요. #include <functional>을 해도 std::function라는 명칭 자체도 없고요. C++11 표준이 일부 반영되어 있음에도 불구하고 그쪽은 지원되지 않는 듯합니다.

    그나저나,
    1. 본이 아니게 댓글이 무진장 길어졌는데, 이거 조만간 본문으로 옮길 예정.
    2. 링크가 걸리는 URL도 있고, 안 걸리는 URL도 경험상 있는데, 정확한 차이는 저도 잘 모르겠습니다.

    1. 김 기윤 2012/05/26 00:56 # M/D Permalink

      후후(?).. 덕분에 저도 즐겁게 배우고 갑니다.

      댓글에서 즐겁게 놀아본 건 오랜만인 듯 하기도 하구요.

    2. Lyn 2012/05/29 10:04 # M/D Permalink

      C++의 람다도 std::function<형식> f = 람다식; 처럼 가능합니다.

      C++에서의 클로저는... 그냥 functor랑 내부구현은 비슷하다고 보셔도 틀리지 않을겁니다.
      캡쳐한 변수를 멤버로 가지는 구조체를 만들고 this->func() 을 호출해버리는거죠

  5. 김재주 2012/05/25 20:07 # M/D Reply Permalink

    람다 함수는 뭐 자바스크립트 루비 파이썬 등등 요새 많이 사용되는 언어에는 거의 있죠. 근데 C++에도 들어가게 되다니 이거 참...... 제가 컴파일러 쪽은 잘 모르다보니 어떻게 인터프리터 언어도 아닌 C++에서 어떻게 클로져를 구현한 건지 모르겠네요. 생각 좀 해봐야 되나

  6. 김재주 2012/05/26 10:40 # M/D Reply Permalink

    아 그런데 C++의 람다함수는 자기 자신을 호출할 방법이 없나요? 자바스크립트의 경우는

    var f = function factorial(n) { if(n==1) return 1; else return factorial(n-1) * n;} 같은 식으로 선언이 가능한데요

    1. 사무엘 2012/05/26 16:48 # M/D Permalink

      아무래도 이름이 없는 함수라면 자기 자신을 일컬을 방법이 특별한 예약어라도 있지 않으면 없겠지요?
      그건 저도 잘 모르겠네요.

  7. Scavenger 2012/06/01 10:58 # M/D Reply Permalink

    잘보고 갑니다. 다음에는 C++11의 큰 변화 중 하나인 R-Value 에 대해서도 다루어주세요.

    1. 사무엘 2012/06/01 12:19 # M/D Permalink

      반갑습니다. R-value 참조자에 대해서는 바로 얼마 전에 쓴 글에 다뤄져 있습니다. 참고하세요. ^^
      http://moogi.new21.org/tc/683

Leave a comment

지금은 C++11이라고 개명된 C++ 확장 규격인 C++0x에는 잘 알다시피 여러 참신한 프로그래밍 요소들이 추가되었다. 몇 가지 예를 들면, 상당한 타이핑 수고를 덜어 줄 걸로 예상되는 auto 리뉴얼, 숫자와 포인터 사이의 모호성을 해소한 nullptr, 그리고 숫자와 enum 사이의 모호성을 해소한 enum class가 있다.

그런데 이것 말고도 C++11에는 아주 심오하고도 재미있는 개념이 하나 또 추가되었다. 복사 생성자에 이은 이동 생성자, 그리고 이를 지원하기 위한 type modifier인 &&이다. R-value 참조자라고 불린다. 이 글에서는 이것이 왜 도입되었는지를 실질적인 코드를 예를 들면서 설명하겠다.
다음은 생성자에서 주어진 문자열의 복사본을 보관하는 일만 하는 아주 간단한 클래스이다.

//typedef const char*  PCSTR;
class MyObject {
    PCSTR dat;
public:
    MyObject(PCSTR s): dat(strdup(s)) {}
    ~MyObject() { free( const_cast<PSTR>(dat) ); }
    operator PCSTR() const { return dat; }
};

C++은 언어 차원에서 포인터를 자동으로 관리해 주는 게 전혀 없다. 그렇기 때문에 저렇게만 달랑 짜 놓은 클래스는 함부로 값을 대입하거나 함수 호출 때 개체를 reference가 아닌 value로 넘겨 줬다간, 동일 메모리의 다중 해제 때문에 프로그램이 jot망하게 된다. C++ 프로그래머라면 누구라도 위의 코드의 문제를 즉시 알 수 있을 것이다.

그렇기 때문에, 포인터처럼 외부 자원을 따로 가리키는 클래스는 복사 생성자와 대입 연산자를 별도로 구현해 줘야 한다. 구현을 안 할 거면 하다못해 해당 함수들을 빈 껍데기만 private 형태로 정의해서 접근이 되지 않게 해 놓기라도 해야 안전하다.

MyObject(const MyObject& s): dat(strdup(s))
{
    puts("복사 생성자");
}
MyObject& operator=(const MyObject& s)
{
    free(dat); dat=strdup(s.dat); puts("복사 대입");
    return *this;
}

자, 그럼 이를 이용해 그 이름도 유명한 Swap 루틴을 구현해서 복사 생성자와 대입 연산자를 테스트해 보자.

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

int main()
{
    MyObject a("새마을호"), b("무궁화호");
    printf("%s(%X) %s(%X)\n", (PCSTR)a,(PCSTR)a, (PCSTR)b,(PCSTR)b);
    Swap(a,b);
    printf("%s(%X) %s(%X)\n", (PCSTR)a,(PCSTR)a, (PCSTR)b,(PCSTR)b);
    return 0;
}

프로그램의 실행 결과는 다음과 같은 식으로 나올 것이다.

새마을호(181380) 무궁화호(181390)
복사 생성자
복사 대입
복사 대입
무궁화호(1813B8) 새마을호(1813D0)

복사 생성자와 대입 연산자 덕분에 메모리 관리는 옳게 되었기 때문에, 이제 프로그램이 뻗는다거나 하지는 않는다.
그러나 이 방법은 비효율적인 면모가 있다. 개체의 값을 맞바꾸기 위한 세 번의 연산 작업 동안, 당연한 말이지만 메모리 할당과 해제, 그리고 문자열의 복사가 매번 발생했다. 그래서 비록 문자열 값은 동일하지만 그 문자열이 담긴 메모리 주소는 a와 b 모두 예전과는 완전히 다른 곳으로 바뀌었음을 알 수 있다.

이때 R-value 참조자를 쓰면, 이 클래스에 대해서 Swap 연산이 메모리를 일일이 재할당· 복사· 해제하는 게 아니라 a와 b가 가리키는 문자열 메모리 주소만 간편하게 맞바꾸도록 하는 언어적인 근간을 마련할 수 있다. 기존 참조자는 &로 표현하고, 이와 구분하기 위해 R-value 참조자는 &&로 표현된다. 참조자(&)는 포인터(*)와는 달리 다중 참조자(참조자의 참조자) 같은 개념이 없기 때문에, &&을 이런 식으로 활용해도 문법에 모호성이 생기지 않는다.

& 대신 &&를 이용해서 자신과 동일한 타입의 개체를 받아들이는 생성자와 대입 연산자를 추가로 정의할 수 있다. 이 경우, 이들 함수는 복사가 아닌 이동 생성자와 이동 대입 함수가 된다. 아래의 예를 보라.

MyObject(MyObject&& s)
{
    dat=s.dat, s.dat=NULL; puts("이동 생성자");
}
MyObject& operator=(MyObject&& s)
{
    //주의: 실제 코드라면 자기 자신에다가 대입하는 건 아닌지 체크하는
    //로직이 추가되어야 한다. if(&s!=this)일 때만 수행하도록.
    free(dat); dat=s.dat, s.dat=NULL; puts("이동 대입");
    return *this;
}

복사 버전과는 달리, strdup 함수 대신 그냥 포인터 대입을 썼음을 알 수 있다. 이것이 핵심이다.
그러면 s가 가리키던 메모리 영역이 내 것이 된다. 그 뒤 s가 가리키던 메모리는 NULL로 없애 줘야 한다. free 함수는 그 스펙상 자체적으로 NULL 체크를 하기 때문에, 소멸자 함수는 그대로 놔 둬도 된다.

즉, 이동 생성자와 이동 대입은 s의 값을 내 것으로 설정하긴 하나, 그 과정에서 필요하다면 s의 내부 상태를 건드려서 바꿔 놓을 수 있다. 그렇기 때문에 복사 생성자/대입과는 달리 s가 const 타입이 아니다.

이것만 선언해 줬다고 해서 Swap 함수의 동작 방식이 이동 연산으로 곧장 바뀌는 건 물론 아니다. 그랬다간 s의 상태가 바뀌고 프로그램 로직이 달라져 버리기 때문에, 컴파일러가 섣불리 동작을 바꿀 수 없다. 그렇기 때문에 Swap 함수의 코드도 move-aware하게 살짝 고쳐야 한다.

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

즉, 개체를 생성하고 대입하는 곳에서, 가져오는 개체를 가능한 한 move로 취급하라고 명시적인 형변환을 해 줘야 한다. 이렇게 해 주고 나면 드디어 우리의 목표가 이뤄진다!

새마을호(181380) 무궁화호(181390)
이동 생성자
이동 대입
이동 대입
무궁화호(181390) 새마을호(181380)

물론, 저런 형변환 연산이 보기 싫은 사람은 <vector>에 정의되어 있는 std::move 함수로 이동 대입을 해도 되며, 보통 R-value 참조자를 설명해 놓은 인터넷 사이트들도 그 함수를 곧장 소개하고 있다. 하지만 그 함수의 언어적인 근거가 바로 이 문법이라는 건 알 필요가 있다.

생성이나 대입에서 R-value 참조자를 받지 않고 기존의 L-value 참조자만 받는 클래스에 대해서는, 이동 대입이나 생성도 자동으로 옛날처럼 복사 대입이나 생성 방식으로 행해진다.
다시 말해, Swap 함수의 로직을 저렇게 고치더라도 R-value 참조자가 구현되어 있지 않은 기존 타입들에 대한 동작은 전혀 바뀌지 않으며 컴파일 에러 같은 게 나지도 않는다. 그러니 호환성 걱정은 할 필요가 없다.

그리고 이미 눈치챈 분도 있겠지만, MFC의 CString처럼 자기가 가리키는 메모리에 대해서 자체적으로 reference counting을 하고 copy-on-modify 같은 테크닉을 구현해 놓았기 때문에, 어차피 복사 생성이나 call by value 때 무식한 오버헤드가 발생하지 않는 클래스라면, 구태여 이동 생성자나 이동 대입 연산자를 또 구현할 필요가 없다. 이동 생성/대입은 언제까지나 기존의 복사 생성/대입을 보조하기 위해서 도입되었기 때문이다.

특히 std::vector 같은 배열 컨테이너 클래스에다가 덩치 큰 개체를 집어넣거나 뺄 때 복사 생성자가 쓸데없는 오버헤드를 발생시키는 걸 막는 게 이 문법의 주 목적이다. 그렇기 때문에 딱히 smart한 복사 메커니즘을 갖추고 있지 않은 클래스를 STL 컨테이너에다 집어넣고 쓰는 C++ 코드라면, 적절한 이동 생성자와 대입 연산자를 구현해 주고 R-value 참조자를 지원하는 최신 C++ 컴파일러로 다시 빌드를 하는 것만으로도 성능 향상을 경험할 수 있다.

예전에는 배열 컨테이너 클래스들이 원소들의 일괄 삽입이나 삭제를 위해 무식한 memmove 함수를 내부적으로 쓰는 게 불가피했는데 이 역할을 이동 대입이 어느 정도 대체도 할 수 있게 됐다.
&&을 DLL symbol로 표기하기 위한 새로운 C++ type decoration도 별도로 물론 있다.

그런데 의문이 생긴다. &&의 이름이 왜 R-value 참조자인 것일까?
이 참조자는 참조자이긴 하지만, 오리지널 참조자처럼 L-value가 아니라 R-value를 취급하라고 만들어졌기 때문이다. L-value, R-value란 무엇인가? 대입문에서 좌변과 우변을 뜻한다. L-value란 값을 갖고 있으면서 동시에 대입의 대상이 될 수 있는 변수를 가리키며, R-value는 값을 표현할 수만 있지 그 자신이 다른 값으로 바뀔 수는 없는 상수, 혹은 임시 개체를 가리킨다고 보면 얼추 맞다.

아래의 코드에서 볼 수 있듯 기존 L-value 참조자는 dereference된 포인터와 같은 역할을 한다.

int& GetValue() { … }
GetValue() = 100;

int *GetValue() { … }
*GetValue() = 100;

그렇기 때문에 아래와 같은 특성도 존재한다.

void GetValue2(int& x) { x=… }

int a;
GetValue2(a); //a는 L-value이므로 OK
GetValue2(500); //에러. 당연한 귀결임

L-value 참조자가 상수값 내지 임시 생성 개체 같은 R-value를 함수의 인자로 받아들이려면, 해당 참조자는 const로 선언되어서 값의 변경이 함수 내부에서 발생하지 않는다는 보장이 되어야 한다. int&가 아니라 const int&로 말이다.

그런데 R-value 참조자는 const 속성 없이도 임시 개체나 상수값을 받아들이며, 그걸 뒤끝 없이 자유롭게 고칠 수 있다. 위의 GetValue2 함수가 int&&로 선언되었다면, 반대로 a를 전달한 게 에러가 나고 500을 전달한 건 괜찮다. a를 전달하려면 static_cast<int&&>(a)로 형변환을 해 줘야 한다. 그러면 마치 int&인 것처럼 실행되긴 한다.

R-value 참조자로 돌아온 함수의 리턴값은 말 그대로 R-value이기 때문에 대입 가능하지 않다. 그렇기 때문에 아래의 코드는 에러를 일으킨다. (R-value 참조자의 리턴값은 당연히 그 역시 R-value로 왔을 때에만 의미가 있을 것이다.)

int&& GetValue3() { … }
GetValue3() = 100; //에러

이런 R-value 참조자라는 괴상망측한 개념은 왜 도입된 것일까? 그리고 이게 앞서 이 글에서 언급한 이동 생성자/대입 연산하고는 도대체 무슨 관련이 있는 것일까?

R-value 참조자의 형태로 함수 인자로 넘어온 개체는 그 함수의 실행이 끝난 뒤엔 어차피 소멸되고 없어질 것이기 때문에 내부가 바뀌어도 상관없다. 즉, 이 참조자는 태생적으로 const 속성과는 어울리지 않는다. 오히려 const-ness가 보장되지 않아도 되는 제한적인 문맥에서, 쓸데없는 복사를 할 필요 없이 꼼수를 좀 더 합법적으로 구사할 수 있게 위해 이런 문법이 추가되었다고 보는 게 타당하다.

마지막으로 R-value 참조자가 유용하게 쓰이는 용도를 딱 하나만 더 소개하고 글을 맺겠다.
윈도우 API+MFC 기준으로, RECT 구조체를 받아서 이 값을 적당히 변형한 뒤에 이를 토대로 후처리를 하는 함수를 생각해 보자.

void Foo(const RECT& rc)
{
    RECT rc2 = rc; //rc는 const이기 때문에 복사본을 만들어야 함

    ::OffsetRect(&rc2, x,y); //변형
    ::DrawText(hDC, strMsg, -1, &rc2, 0);
}

void Foo(RECT&& rc)
{
    ::OffsetRect(&rc, x,y); //복사본 만들 필요 없이 rc를 곧바로 고쳐서 사용하면 됨
    ::DrawText(hDC, strMsg, -1, &rc, 0);
}

CRect r(100, 200, 400, 350);
Foo(r); //const RECT& 버전이 호출됨
Foo( CRect(0,0, 400,300) ); //임시 개체임. RECT&& 버전이 호출됨

RECT를 value로 전달했다면 당연히 복사가 일어나고, const reference로 전달했다면 역시 복사가 행해져야 한다. 그러나 애초에 함수에 전달되는 인자가 임시 개체였다면, 임시 개체에 대한 복사본을 또 만들 필요 없이 그냥 그 임시 개체를 바로 고쳐 쓰면 된다. 위의 코드의 의미가 이해가 되시겠는가?

R-value 참조자라는 게 왜 필요한지, 그리고 이게 왜 이동 생성/대입과 관계가 있는지 본인은 이해하는 데 굉장히 긴 시간이 걸렸다. 인터넷에 올라와 있는 다른 설명만 읽어서는 도통 이해가 되지 않아서 직접 코드를 돌리고 컴파일을 해 본 뒤에야 개념을 깨우쳤는데, 알고 나니 정말 이런 걸 생각해 낸 사람들은 천재라는 생각이 든다.;; C++은 참으로 복잡미묘한 언어이다.

Posted by 사무엘

2012/05/16 08:41 2012/05/16 08:41
,
Response
No Trackback , 15 Comments
RSS :
http://moogi.new21.org/tc/rss/response/683

Trackback URL : http://moogi.new21.org/tc/trackback/683

Comments List

  1. Lyn 2012/05/16 11:06 # M/D Reply Permalink

    move constructor 만쉐~

  2. Lyn 2012/05/16 11:08 # M/D Reply Permalink

    레퍼런스->RValue 레퍼런스로 변경해주는 std::move 함수가 추가되었습니다 : )

    1. 사무엘 2012/05/16 15:01 # M/D Permalink

      네, 이동 생성자는 멋진 개념이죠. 그리고 본문에도 그 함수가 언급돼 있습니다. ^^

  3. 아라크넹 2012/05/16 11:55 # M/D Reply Permalink

    r-value reference의 또 다른 중요한 사용 용도는 메소드를 다른 객체의 메소드로 forwarding하는 경우가 있습니다. 사실 맨 마지막 예시의 변형이긴 하네요.

    struct A { void foo(std::string, std::string&); };
    struct B { A a; void foo(std::string, std::string&); };
    void B::foo(std::string arg1, std::string& arg2) { a.foo(arg1, arg2); }

    이 코드의 경우 B::foo는 A::foo로 forwarding하게 되지만 arg1을 옮기는 과정에서 복사 생성자를 거치게 됩니다. 하지만 B::foo를 다음과 같이 고치면:

    void B::foo(std::string&& arg1, std::string& arg2) { a.foo(std::forward<std::string>(arg1), arg2); }

    복사 생성자를 거치지 않게 됩니다. std::forward는 r-value를 받았으면 r-value를 반환하고 l-value를 받았으면 l-value를 반환하는 거 빼면 아무 변화도 주지 않는 identity function입니다.

    다음 문서를 부분적으로 참고했습니다.
    http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2027.html#Perfect_Forwarding
    http://cpp-next.com/archive/2009/12/onward-forward/

    1. 사무엘 2012/05/16 15:01 # M/D Permalink

      한 문법이 추가되면서 여러 방면의 문제를 한꺼번에 해결할 수 있죠(마치 팔방미인이 된 auto처럼). R-value 참조자도 그런 개념인 것 같습니다.

  4. Lyn 2012/05/16 15:34 # M/D Reply Permalink

    점점 코드가 암호화되어가고있습니다 ㅡㅜ

    이제 대입버그도 두군데서 나오겠군요

  5. 김 기윤 2012/05/16 19:59 # M/D Reply Permalink

    C++11 스펙에서 봤던 것이긴 한데, 제대로 이해는 못 했었고, 지금도 제대로 이해를 못 했습니다(.....) 단지 좋은거다~ 라는 정도만 이해하고 있을 뿐.. orz..

    1. 사무엘 2012/05/17 10:28 # M/D Permalink

      요즘 스마트폰 때문에 뭐든지 '스마트'해졌다고 그러는데, R-value 참조자는 C++ 언어의 디자인과 C++ 라이브러리의 동작을 좀더 '스마트'하게 개선하는 효과가 있는 듯합니다. ^^
      물론 C++은 처음 접하는 사람에게는 갈수록 더 배우기 어려운 복잡한 언어가 돼 가고 있죠.

    2. Lyn 2012/05/17 11:22 # M/D Permalink

      한번쓰고 버릴 데이터를 대입할때 복사가 일어나지 않도록 한다!

      라는것만 이해하면 나머진 쉽더라구요.
      물론 전 사무엘님보다 능력이 딸려서 거의 1주일을 문서 들여다보고 고민 ㅜㅜ

    3. 김 기윤 2012/05/17 18:29 # M/D Permalink

      오히려 람다가 훨씬 쉬워요 orz..

  6. 주의사신 2012/05/17 17:12 # M/D Reply Permalink

    졸업 작품 만들 적에 VC++ 10에 추가된 C++11을 많이 사용했는데, 안 그래도 어려운 C++이 더 어려워지는 느낌을 조금 받기는 했습니다.

    1. 사무엘 2012/05/18 10:58 # M/D Permalink

      “나의 C++은 이런 언어가 아니라능!!” 하는 생각이 들죠. ㄲㄲㄲㄲㄲㄲ

      static 멤버는 선언뿐만 아니라 정의도 밖에서 반드시 해 줘야 하게 바뀜 (1980년대 말)
      템플릿 추가 (1990년대 초중반)
      bool, namespace, typename, explicit, *_cast 등 추가 (1990년대 중반)
      iostream.h에서 .h가 빠지고, C++ 라이브러리들도 std namespace로 이동 (1990년대 중후반)
      export 흑역사
      C++11 (2011년)

      흠, 이런 역사에 대해서 정리해도 글 한편이 될 것 같네요.

  7. 김재호 2012/05/22 00:45 # M/D Reply Permalink

    저는 아라크넹님이 설명한 Perfect forwarding 에서 Perfect라는 단어가 잘 그려지지 않습니다. 마치 영어 시제에서 현재 완료를 말할 때 쓰는 Present Perfect 라는 단어를 접할 때의 맹한 느낌이랄까요.

    그나저나 정리를 잘하셔서 글을 아주 잘 쓰셨네요. 긴 글인데도 참 재밌게 읽었습니다.

    1. 사무엘 2012/05/22 13:37 # M/D Permalink

      재미 있게 읽어 주셔서 고맙습니다. 글 내용이 잘 전달이 됐다니 다행이고요. ^^
      저는 정보력이 아라크넹 님 만하지는 못하지만, 제가 확실하게 아는 내용은 최대한 남이 알기도 쉽게 풀이해서 글로 만들려고 늘 노력합니다.
      또 얼마 후엔 C++11 관련 글이 올라올 겁니다.

  8. Lyn 2012/05/23 13:42 # M/D Reply Permalink

    다음 표준은 가칭이 C++1y라네요 (이번에도 16진수냐?!)

Leave a comment

예전에 본인이 글로 쓴 적도 있고, 상식 차원에서 이미 아시는 분도 있겠지만..
프로그래밍 언어마다 문자열을 다루는 방식엔 차이가 존재한다.
C/C++은 null-terminated 문자열이라는 단순하고 독특한 체계를 사용하는 반면, 다른 언어들은 그렇지 않다.
그렇기 때문에, 문자열 상수가 실행 파일 내부에 어떤 형태로 박혀 있는지를 추적하면, 이 프로그램이 무슨 언어로 만들어졌겠는지 추측이 어느 정도 가능하다.

과거의 도스 시절에는 볼랜드 사에서 개발한 터보 시리즈의 컴파일러가 인기가 많았다. C/C++과 파스칼이 기억에 남는다. 이 볼랜드 제품은 당시 타사의 컴파일러가 제공하지 않던 두 가지 독자적인 기능이 있었다. 하나는 깔끔하게 잘 만들어진 IDE(에디터)였고, 다른 하나는 BGI(볼랜드 그래픽 인터페이스)라고 일컬어지는 그래픽 API였다.

한 IDE에서 프로그램을 바로 빌드-실행-디버그할 수 있으니 프로그램 개발 생산성이 뛰어나고 굉장히 편리하다. 이에 덧붙여, 그래픽은 그렇잖아도 printf 같은 표준화된 API 규격이 전무해서 ‘싸제’ 라이브러리에 의존할 수밖에 없던 영역인데, 자체 개발 라이브러리가 있다 보니 볼랜드의 컴파일러는 폭발적인 인기를 모을 수밖에 없었다.
bgidemo라고 유명한 그래픽 API 예제 프로그램도 있었는데 기억하는 분이 있으려나 모르겠다. QBasic용 예제 프로그램인 nibbles, gorilla 게임과 비슷한 시기에 만들어진 그 시절 추억이다.

사용자 삽입 이미지

아래의 스크린샷은 이 BGI 라이브러리를 사용해서(=링크해서) 만들어진 어느 EXE 파일 내부를 들여다본 모습이다. 그래픽 라이브러리이다 보니 내부적으로 출력하는 에러 메시지 문자열, 가령 No error, (BGI) graphics not installed, 심지어 Out of memory in flood fill 같은 친숙한 문자열이 내장되어 있음을 알 수 있다. 그런데 동일한 문자열들 사이에 한 놈은 ▲, →, ← 같은 이상한 기호가 듬성듬성 끼어 들어가 있다. 왜 그럴까?

사용자 삽입 이미지

사용자 삽입 이미지

기호가 없는 프로그램은 C언어(=터보 C)로 만들어진 프로그램이다. 왼쪽의 16진수값을 보면 알겠지만, 이들은 모든 문자열들이 그냥 0번 문자로 구분되어 있다.
그러나 기호가 있는 프로그램은 파스칼로 만들어진 프로그램이다. ▲, →, ←은 다음에 뒤따르는 문자열의 길이를 의미한다. 예를 들어 “▲Graphics hardware not detected”를 보면 ▲의 코드 번호는 0x1E, 즉 30인데 그 에러 메시지의 길이는 30바이트임을 알 수 있다. 얘네는 반대로 문자열들 사이에 0번 문자가 전혀 존재하지 않는다.

실제로 C/C++ 말고 String이 built-in type으로 존재하는 언어들은 이렇게 글자 수를 따로 저장해 놓는 방식으로 문자열들을 관리한다. 베이직으로 만들어진 프로그램도 QuickBasic이든 PowerBasic이든 문자열 상수들을 들여다보면 비슷한 결과를 얻을 수 있다. 그래서 이런 언어는 문자열의 길이를 구하는 함수의 시간 복잡도가 O(1)인 반면, C언어만 strlen의 시간 복잡도는 O(n)이다.

베이직 언어들은 문자열의 길이가 16비트 정수로 저장되던 반면, 터보 파스칼은 문자열 길이를 달랑 8비트 크기로 저장하여, 문자열의 길이가 256자를 넘을 수 없다는 한계가 존재했다. 흠;;

파스칼로 만든 프로그램을 들여다보면 Runtime error 같은 문자열도 존재한다. 이 역시 C/C++로 만들어진 프로그램에서는 디버그 빌드가 아닌 이상 있을 수 없는 개념이다. C/C++은 배열 첨자 범위의 검사조차도 안 할 정도로 런타임 에러라는 개념 자체가 존재하지 않는-_- 언어이기 때문이다. 그저 컴퓨터 다운(도스 시절)이 아니면 segmentation/page fault(요즘 같은 보호 모드 운영체제에서)-_-만이 존재할 뿐. -_-;;

그 반면, %d, %s이라든가 Null pointer assignment 같은 문자열이 있다면 그건 99.9% C 라이브러리가 들어갔다는 뜻이고 그 프로그램은 C/C++로 작성되었다고 유추할 수 있다.

덧붙이는 말

1. 볼랜드는 BGI 라이브러리만큼이나 텍스트 모드용 GUI? TUI? 툴킷으로 Turbo Vision이라는 라이브러리를 개발한 것으로도 유명했다. MS가 도스용 비주얼 베이직을 잠시나마 개발했다면 볼랜드에는 이런 게 있었던 셈. 당장 터보 C++과 파스칼의 IDE부터가 이를 사용해서 개발되기 시작했다. 비록 C/C++과 파스칼에서 모두 지원되긴 했지만 이 언어의 주 개발 및 지원 언어는 파스칼이었지 싶다. MS가 베이직을 좋아한다면, 볼랜드는 전통적으로 파스칼을 더 좋아하는 회사였다. (그러니까 훗날 델파이까지 만들었지)

지금은 세월이 세월이다 보니 소스가 완전히 풀려서 이이 프로젝트는 오픈소스 진영에서 관리되고 있다. 내 기억이 맞다면 DJGPP의 IDE인 Rhide가 이 Turbo Vision의 오픈소스 버전으로 개발되었다.
그리고 우리나라에서 PC 경진대회가 정보 올림피아드로 최초로 바뀌었던 1996년(13회), 대회의 채점 프로그램이 Turbo Vision 기반으로 개발되어 있던 걸 본인은 분명히 봤다.

2. 오늘날 윈도우용 네이티브 EXE/DLL이 만들어지는 출처는, 내 감으로는 비주얼 C++이 적게 잡아도 70% 이상, 그 뒤에 소수의 오픈소스 프로젝트용으로 gcc, 그리고 끝으로 델파이 정도가 고작인 것 같다. 볼랜드는 그 후로 다른 회사에 인수되면서 이름도 여러 번 바뀌고(InPrise, CodeGear, Embarcadero 등...;;) 우여곡절을 많이 겪었는데 걔네 입장에서는 옛날의 영광이 그리울 법도 할 것 같다.

3. BGI 라이브러리와 파워베이직--얘 역시 전신이 볼랜드 사의 터보 베이직이긴 했지만--의 그래픽 라이브러리는 이상하게도 VGA mode 13h를 지원하지 않아서 개인적으로 아쉬웠었다. (퀵베이직은 지원했는데...) 해상도가 너무 낮아서 한글· 한자 같은 문자를 찍는 데는 부적격이었지만 256색 덕분에 게임 만들 때는 필수이던 그래픽 모드이다. 그게 지원됐으면 그 당시 게임 만들기가 훨씬 더 수월했을 텐데 말이다.

Posted by 사무엘

2011/07/15 08:38 2011/07/15 08:38
, , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/540

Trackback URL : 이 글에는 트랙백을 보낼 수 없습니다

Comments List

  1. 주의사신 2011/07/15 09:36 # M/D Reply Permalink

    터보 파스칼, 델파이를 만드신 개발자님(앤더슨 헬스버그)께서 MS에 가셔서 C#과 닷넷을 만들어 내셨죠.

    MS에서 스카웃할 때 그냥 받고 싶은 만큼 적어 오라고, 백지 수표 한 장을 주었다는 전설이 전해져 옵니다....

    1. 사무엘 2011/07/15 16:25 # M/D Permalink

      그 반면, 왕년에 볼랜드에서 Turbo Basic을 잠시 개발하던 분은, 볼랜드 퇴사 후 PowerBasic 창업을 했죠. ^^;;

Leave a comment
« Previous : 1 : 2 : 3 : 4 : 5 : 6 : 7 : Next »

블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2019/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:
1219375
Today:
66
Yesterday:
531