예전에도 몇 차례 얘기했듯이 비주얼 C++은 지금까지 내 인생에서 가장 재미있는 장난감이요, 친구요, 자아실현 매체요, 생계 수단 역할을 톡톡이 해 왔다.

비주얼 C++은 여느 프로그래밍 툴과는 다르게 뭐랄까, standalone, independent이고 자가생성이 가능하다. 쉽게 말해서 비주얼 C++ 자신과 같은 레벨의 컴파일러/런타임/IDE 같은 프로그램을 비주얼 C++로 또 만들 수 있다는 뜻이다. 실제로 마소에서 비주얼 C++은 이전 버전의 비주얼 C++로 만들고 있기도 하고. 이렇듯, 이 툴은 가장 배우기 어렵지만 가장 강력하고 군더더기 없는 프로그램을 만들 수 있다.

2014년 현재, 난 한 컴퓨터에 다음과 같은 세 버전을 깔아 놓는다. 제각기 필요와 쓸모가 있기 때문이다.

1. 2003

  • 2010에서 새로 도입된 Help Viewer가 완전 거지 같아서.. 단순 윈도 API나 MFC 레퍼런스를 조회하는 덴 200x 구버전 document explorer 기반의 msdn이 짱이다. (1) 색인이 처음에 뜨는 데 시간이 너무 오래 걸리는 것, (2) 가끔 목차/색인을 클릭해도 해당 항목 문서가 안 나타나는 것--정확히는 수 초 뒤에 한참 뒤에 뜸.. 이 두 버그 때문에 학을 뗐다. (단, 2012 이후의 Help Viewer 2.0은 불편하던 게 좀 개선된 거 같기도 하고..)
  • 2003은 MFC가 지금처럼 말도 안 되게 bloat되기 전이며, 굉장한 legacy 운영체제에도 돌아가는 바이너리를 만들 수 있는 버전이다. <날개셋> 타자연습을 여전히 10년 전의 구닥다리 컴파일러로 빌드하는 이유가 이것 때문이다.
  • 다만 2003은 IDE가 빌드 내지 리소스 편집 중에 잘 뻗는 편이고(불안정!) Vista 이후 OS에서는 일부 기능이 충돌도 함. 조심해서 써야 한다.

2. 2010

  • 닷넷 이래로 Visual Studio가 기본 제공하던 msi 설치/배포 프로젝트 기능이 2012에서 갑자기 없어져 버린 관계로, 2010을 도저히 제거할 수가 없게 됐다. 대체품이라는 InstallShield 번들 에디션은 어마어마한 덩치와 복잡한 사용법 때문에 곧바로 gg 치고 언인스톨해 버렸다.
  • 또한 <날개셋> 한글 입력기는 빌드와 관련된 특이한 이슈 때문에 2012가 아닌 2010 컴파일러 툴체인을 사용하고 있다.
  • 다만, 2010은 IDE의 비주얼이 역대 VC++ 역사상 제일 구리고 우중충 칙칙하고 안 좋았다. -_-;;

3. 2012

난 201x가 다음과 같은 점에서 마음에 든다. (1) 크게 강화된 인텔리센스 엔진 (2) 람다 같은 C++ 최신 문법 (3) 빌드나 리소스 편집 중에 IDE가 이제 거의 뻗지 않음
2012는 이를 바탕으로 2010보다 훨씬 더 깔끔한 GUI에, 신택스 컬러링도 훨씬 더 강화되어 몹시 마음에 든다. 몇 가지 크리티컬만 없었으면 2012가 2010을 완전히 대체할 수도 있었을 텐데. ㅜ.ㅜ
다만 2012 얘만 꼭 남겨 둘 이유 역시 없기 때문에 이것보다 더 최신 버전이 나오면 그걸로 대체할 수도 있다. 즉, 2012는 2003/2010과는 달리 고정 보존 상태는 아니다.

위와는 달리, 보존 대상에서 제외되고 안 쓰는 버전은 다음과 같다.

1. 6.0

VC6은 그야말로 개발툴계의 IE6이나 마찬가지다. 출시 시기는 다르지만 공교롭게도 버전 번호도 동일하고 말이다. IE가 윈도 비스타의 출시 지연 때문에 6 이후로 5년 가까이 버전업이 없었다면, VC는 닷넷이 첫 개발되느라 4년 가까이 6 이후로 버전업이 없었다. 그 뒤 지나치게 오랫동안 현역을 뛰어 왔다.

웹 개발자들이 제발 IE6 좀 퇴출시키자고 캠페인 하는 것만큼이나 PC 클라이언트 개발자들은 업계에서 VC6 좀 퇴출시키자고 캠페인이라도 해야 할 판이다. 단지, IE는 모든 PC 사용자들이 쓰는 웹브라우저인 반면, VC는 극소수 프로그래머만이 쓰는 개발툴이라는 점이 다르다.

VC6은 이제 해도 해도 너무하다 싶을 정도로 심하게 후지고 낡았다. IDE가 IME-aware하지도 않고, 특히 한글 윈도에서는 기본 글꼴이 윈도 3.1 스타일의 완전 추레한 System으로 나옴! 인텔리센스는 지금에 비하면 완전 안습 크리 수준이고. 최신 C++ 표준이나 멀티코어 같은 건 아웃 오브 안중이다.

VC6이 아니면 도저히 빌드시킬 수 없는 비표준 코드가 이미 수십만 줄 이상 작성되어 버려서 도저히 수습을 못 할 지경이 된, 한 20년 묵은 불가피한 프로젝트가 아니라면 아직까지 VC6을 고집할 이유란 없어야 정상일 것이다. for문 변수 scope 정도는 후대의 컴파일러로도 옵션을 바꿔서 수용시킬 수 있을 텐데.

굳이 장점을 찾자면, VC6은 생성되는 바이너리가 운영체제의 MSVCRT와 MFC42를 직통 지원한다는 점이 매우 유리하다. 그러나 이것도 어차피 64비트는 지원 안 하기 때문에 장점이 반쪽짜리 이하로 의미를 크게 상실한다.

2. 2005

MS 오피스 2003이 아닌 독자 GUI 비주얼을 선택한 첫 버전 되시겠다. (VC 2005가 오피스 2003 같은 시퍼런 비주얼 기반이었다면? 상상만 해도 ㅎㅎ)
난 얘는 일단 sp1과 운영체제 패치를 설치하는 시간이 2005 자체를 설치하는 데 걸리는 시간보다 더 길어서 인상이 매우 안 좋다. 게다가 CRT/MFC DLL 배포 방식도 구리게 바뀌었고. 장점은 어차피 (1) 2003이나(msdn 등) (2) 이후 버전(64비트 지원 등)에 다 포함돼 있기 때문에 굳이 얘가 필요하진 않다. out.

3. 2008

2005보다는 훨씬 더 괜찮은 물건이고 쓸 만하다. 그리고 은은한 연보라색 톤(비스타/7 기준)의 IDE 외형은 역대 버전들 중 가장 깔끔하고 괜찮았다고 생각한다.
200x 중에서는 가장 훌륭했지만, 역시 얘만 보존해야 할 필요는 존재하지 않는다. 플러스 팩의 등장과 함께 MFC가 완전 bloatware로 바뀌어 버렸고, CRT/MFC DLL 배포 방식은 여전히 아쉬운 점이다.

위의 두 카테고리 말고 본인이 special case로 예우하는 골동품 버전이 있는데, 그건 6.0보다도 더 옛날 버전인 4.2이다. mfc42의 원조인 바로 그 버전이다.
본인이 난생 처음으로 구경한 비주얼 C++ 버전이어서 애착이 간다.

Posted by 사무엘

2014/08/07 08:28 2014/08/07 08:28
, , ,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/993

C/C++에서 구조체나 클래스는 통상적으로 global scope에서 선언되거나 기껏해야 다른 클래스 내지 namespace의 내부에서 선언된다. 즉, 어차피 비실행문들만 있는 곳에서 선언되는 편이다.
그러나 실행문으로 이뤄져 있는 함수 안에서 이들을 새로 선언하는 것도 문법적으로 가능하다. 다시 말해, 변수를 선언하는 것뿐만 아니라 그 변수들의 type을 결정하는 구조체나 클래스를 즉석에서 선언해 쓰는 것도 가능하다는 뜻이다.

다른 곳에서 두고두고 재사용하는 구조체가 아니라 함수 한 곳에서 튜플 같은 형태로 잠깐만 사용하고 마는 구조체라면, 이런 식으로 함수 안에서 간단히 선언해서 사용하면 좋다. 하긴, 그러고 보니 struct, class, union, enum뿐만 아니라 typedef도 실행문과 비실행문 문맥에서 모두 사용 가능한 물건이다.

함수 안에서 클래스 같은 걸 따로 선언하는 건 C#에서는 가능하지 않으며 C++만의 전유물인 듯하다.
이렇게 함수에서 선언된 자료형은 유효 범위도 마치 지역변수처럼 그 함수 안으로 완전 local하게 한정된다. 그래서 각종 IDE 같은 데에 명칭이 뜨지도 않는다. 지금부터는 이와 관련해서 좀 더 기괴한 이야기들을 풀어 보겠다.

1. 무명 자료형

C/C++에는 '이름 없는' 구조체/클래스/공용체 따위의 개체가 있을 수 있다. '이름 없는' 함수 그 자체만의 선언은 지원되지 않아서 함수형 프로그래밍 패러다임이 도입된 C++0x 이후에서야 람다와 함께 등장한 반면, 이름 없는 복합 자료형이라는 개념은 있었던 것이다.

class {
public:
    int x,y,z;
} obj;

C#이나 자바 스타일이라면 상상도 할 수 없는 일이겠지만, C/C++은 자료형의 선언과 해당 자료형에 속하는 변수의 선언을 동시에 할 수 있다. class OBJ { ... } a,b,c; 도 OBJ a,b,c;나 심지어 int a,b,c;와 개념적으로 같다.
class, struct, enum, union 등을 선언한 뒤에는 닫는 중괄호 다음에 세미콜론을 반드시 붙여야 하는 이유가 바로 이 때문이다.

그런데 이름 없는 자료형은 자료형의 선언과 함께 변수 선언도 같이 해 주는 게 선택이 아닌 '필수'라는 차이가 있다. 그도 그럴 것이, 얘는 이름이 없는 일회용 자료형인 고로 그 자료형을 선언하는 구문이 끝난 뒤에는 그걸 지칭할 방법이 없기 때문이다. 변수가 단 하나라도 같이 선언돼 있어야 나중에 C++11의 auto 라도 써서 그것과 동일한 자료형의 변수를 추가로 만들 수 있을 것이다.

이런 무명 자료형이라는 개념은 대개 한 자료구조 내부에서 구조체와 공용체를 섞어 가며 쓸 때 유용하지만, 그렇잖아도 일회용 성격이 강한 local 자료형에서도 더욱 의미가 있다. 굳이 이름을 생각할 필요 없이 내가 생각하는 복합 자료형을 간단하게 만들어서 쓰게 해 주기 때문이다.
물론 local뿐만 아니라 global scope에서도 무명 자료형을 얼마든지 선언해서 쓸 수 있다. C/C++의 오묘한 면모 중 하나이다.

2. 함수 안에 함수

C/C++은 복합 자료형은 앞서 살펴보았듯이 무명으로 선언할 수 있고, 그 안에 또 다른 복합 자료형을 nested된 형태로 선언하고 집어넣을 수 있다. 그러나 실행되는 코드의 집합인 함수를 그렇게 일종의 값처럼 자유자재로 다룰 수 있지는 않았다.

함수 자체를 다른 함수에다가 전달하는 것은 그나마 함수 포인터가 있으니 불가능하지는 않지만, 그건 자료형, 함수명 등에 대한 작명이 필요하며 기계 중심적이고 융통성이 부족했다. 또한 함수 안에다가 또 일회용으로 간단히 쓰고 마는 함수를 잠깐 선언하는 것도 가능치 않아서 global/class scope 차원에서의 선언이 필요했다. 남는 건 매크로 함수밖에 없지만 이게 얼마나 구조적으로 허접한 물건인지는 역시 설명이 필요하지 않는 수준이고.

void func()
{
    void simple_func(int x) { }

    simple_func(0);
    simple_func(1);
}

nested function은 C와 파스칼의 큰 차이 중 하나이기도 했다. 파스칼은 지원하지만 C/C++ 계열은 지원하지 않았기 때문이다. 마치 가변 길이 배열만큼이나 언어 차원에서 결코 지원되지 않을 금기 봉인 사항이기라도 한가 궁금하다. 다만, 옛날에 gcc던가 극소수 C 컴파일러에서 확장 옵션을 통해서 nested function을 지원하는 걸 본 것 같다.

물론, 중첩 함수를 써서 할 수 있는 일은 중첩 함수라는 개념이 없이도 완전히 똑같이 할 수 있기 때문에 상호 등가 교환이 가능하다. 마치 클래스에서 public과 private 구분을 해 주든, 아니면 전부 싸잡아 public인 struct로 코드를 작성하든.. 이것은 코드의 유지 관리의 편의성 내지 정보 은닉하고만 관계가 있지 프로그래밍 언어의 구조적인 계산 능력과는 무관한 것하고 같은 맥락이다. 그래서 C/C++은 nested 함수라는 개념을 도입하지 않은 듯하다. 정수 타입에 subrange 같은 개념도 없을 정도이니 뭐~

지금이야 람다 덕분에 함수 안에 함수의 선언이 사실상 가능해졌다. 캡처 같은 새로운 개념도 같이 도입됐다. 하지만 이건 일반적인 함수와 개념적으로 같은 물건은 아니다.
C++에서는 (1) 중첩 namespace 안에 들어있는 함수가 얼추 비슷한 개념일 수 있으며, 이것 말고도 좀 더 직접적으로 함수 안에 함수를 만드는 것이 편법 우회 경로로 가능하다. (2) 바로 함수 안에 클래스를 선언하고 멤버 함수를 정의하는 것이다. 이런 식으로.

int main(int argc, char* argv[])
{
    class A {
    public:
        static void Func() { puts("function inside function"); }
    };
    A::Func();
    return 0;
}

특히 static 함수는 this 포인터를 사용하지도 않으니 진짜로 일반 함수와 다를 바가 없다.
함수 안에다 구조체를 정의하는 것으로도 모자라서 완전한 형태의 클래스를 정의하고 멤버 함수를 정의하는 것까지도 가능하다니 놀랍지 않은지?

단, 이런 지역 클래스에서 멤버 함수를 선언할 때는 논리적으로 당연한 제약이 하나 걸린다. 함수의 몸체는 반드시 그 클래스 안에서 저렇게 정의되어야 한다. 안 그러면 아까 무명 자료형에서 변수 선언을 바로 안 해 줄 때처럼 경고가 뜬다.

비주얼 C++의 경우 일단 C4822 경고만 뜨고 그걸 실제로 호출까지 한 경우 링크 에러가 났지만, 요즘은 그 즉시 C3640 에러도 같이 나오는 듯. 링크 에러가 더 친절하게 컴파일 에러로 바뀌었다.
클래스의 밖인 그 함수 몸체 안에서 또 void A::Func() { } 이런 식으로 함수 몸체를 따로 정의하는 건 문법적으로 허용되지 않기 때문이다.

또한, 이런 이유로 인해, 지역 클래스는 static 멤버 함수는 가질 수 있는 반면 static 멤버 변수(=데이터)는 가질 수 없다.
그건 함수 안의 일반 static 변수와 같은 취급을 받으려나 궁금했는데, 만들어 보니 그건 언어 문법 차원에서 허용되지 않으며 곧바로 컴파일 에러가 난다. static const도 허용되지 않는다.

그러고 보니 이름 없는 클래스도 static 멤버 변수를 사실상 가질 수 없을 듯하다. 사실, 이름 없는 클래스에다가 그런 것까지 바라는 것 자체가 변태 도둑놈 심보이긴 하다. ㅎㅎ
멤버 함수야 몸체를 클래스의 선언부 안에다 강제로 집어넣는 식으로 정의할 수 있지만 static 변수는 결국 클래스 밖에서 따로 정의를 해야 하는데, 클래스 이름이 없으니 정의를 할 수 없어서 링크 에러가 나기 때문이다.
이거 정말 복잡한 문제다. C++이 C#/Java하고는 다른 독특 기괴한 면모가 이런 데서 또 발견된다.

Posted by 사무엘

2014/07/25 08:32 2014/07/25 08:32
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/988

하루는 본인은 회사 업무를 위해 인터넷에 굴러다니는 어느 암호화 알고리즘 소스를 프로젝트에다 붙여 쓴 적이 있었다.
그런데 곧장 문제가 발생했다. 본인이 맡은 부분은 Windows용 클라이언트인데, 같은 소스를 사용하는 다른 플랫폼 클라이언트 내지 서버와 교신이 제대로 되지 않고 있었다.

결국은 문제의 코드를 별도의 콘솔 프로그램 프로젝트로 떼어서 따로 돌려 보니, 문제의 원인은 그 암호화 알고리즘에 있음이 밝혀졌다. 같은 소스를 빌드해서 돌렸는데 결과가 서로 차이가 나는 것이었다.
게다가 Visual C++로 빌드하는 같은 Windows용 프로그램도, 알고 보니 debug 빌드는 결과가 옳게 나오는데 release 빌드만이 문제가 있었다!

debug와 release가 서로 다르게 동작하는 프로그램은 십중팔구가 멀티스레드 race condition 아니면 단순 초기화되지 않은 변수 때문이다. 물론 이 코드는 스레드를 따로 만들지는 않으니 의심 부분은 응당 후자. 이거 또 남이 짜 놓은 복잡한 코드에서 꼭꼭 짱박혀 있는 버그 찾느라 무진장 고생하겠다는 생각과 함께 몇 시간 동안 디버깅을 진행했다.

release 모드로 빌드된 프로그램은 함수 인라이닝과 각종 최적화 때문에 debug 빌드처럼 한 라인씩 엄밀하게 step in이 되지 않으며 변수값 조회도 안 되는 경우가 종종 있다. 그러니 도대체 언제부터 두 빌드의 변수값이 달라지는지 printf 신공을 펼치면서 꽤 어렵게 문제 원인을 추적해야 했다.

문제의 범위는 많이 좁혀졌다. stack이나 heap 메모리를 초기화하지 않고 쓴 경우는 눈을 씻고 찾아도 없었다. 마치 난수 씨앗처럼 초기의 동일한 input으로부터 일련의 output들이 계산을 통해 파생되는데, 언제부턴가 두 빌드가 생성해 내는 변수값이 미묘하게 서로 달라지는 게 보였다. 저 동일한 input 말고 계산에 영향을 끼치는 요소는 정말 없는데? 왜 값이 달라지지..?

그리고 결국은 설마 하던 녀석이 사람을 잡았다는 걸 알게 됐다. 문제의 함수는 바로.. 이것이었다!

unsigned long Rol(unsigned long x, long y)
{
    if (y % 32 == 0) {return x;}
    else {return ((x << y)^(x >> -y));}
}

저 간단한 함수의 실행 결과가 release 빌드와 debug 빌드가 서로 달랐다. 비주얼 C++ 2012, 2010, 2003 전부 공통으로.
암호화 알고리즘에서 절대 빠지지 않는 그 이름도 유명한 비트 회전(bit rotation)을 구현한 함수인데..
비트를 음수 shift하는 연산은 좀 생소해 보였다.

본인은 15년 가까이 C/C++ 프로그래밍을 해 오면서 지금까지 막연히 A<<-B = A>>B, A>>-B = A<<B이지 않으려나 생각해 왔다.
그런데 실상은 전혀 그렇지 않았다.
컴퓨터의 구조적인 특성상 나눗셈에서 피연산자의 부호에 음수가 섞이면 몫과 나머지의 부호가 수학에서 생각하는 직관적인 형태로 구해지지 않는다는 건 어렴풋이 알고 있었다만, 비트 shift에도 그런 특성이 있구나.

음수 shift의 결과는 언어 스펙 차원에서 undefined인 모양이다. 진짜 말 그대로 A=A++처럼 '그때 그때 달라요'인 듯.
중의적인 코드를 컴파일러마다 제멋대로 번역하는 것 자체를 모조리 막을 수는 없겠지만, 그건 최소한 '이식성'에 문제가 생길 수 있다고 경고라도 띄워야 하지 않나 싶다.

실제로 위의 함수를 실행하면

Rol(0xBE9F8300, 1);
Rol(0xEC6BFC33, 1);
Rol(0xFC58371A, 1);

의 함수값은 release 빌드에서는 각각 0x7D3F0600, 0xD8D7F866, 0xF8B06E34이 나온다.
그러나 debug 빌드에서는 0x7D3F0601, 0xD8D7F867, 0xF8B06E35가 나오며, 이게 맞는 값이다. release는 무슨 이유에서인지 최하 자리 1비트를 누락하고 있었던 것이다. 그러니 이후의 암호화 결과가 몽땅 틀어지는 건 당연지사.
설상가상으로 xcode에서는 더 이상한 결과가 나왔던 걸로 기억한다.

유명 암호화 라이브러리가 왜 저렇게 이식성 없는 연산을 썼는지 난 잘 모르겠다. 음수 shift의 결과가 어떻게 나올 것을 기대한 건지?
저 문제를 우회하느라 지금까지 머리로만 알고만 있었지 실무에서 쓸 일이 전혀 없으리라 생각했던 테크닉을 쓰게 됐다.
소스 코드의 특정 구간에 한하여 최적화를 잠시 끄는 #pragma optimize("", off) 되시겠다.

bit rotation은 bit shift에다가 한쪽 끝에 있는 비트들을 따로 반대편 끝에다 shift시켜서 얹어 준다는 차이만이 있을 뿐이다. 32비트 부호 없는 정수 기준으로, 작은 자리수가 큰 자리로 이동하는 왼쪽(<<) rotation을 나보고 구현하라면 이렇게 짜겠다.

UINT Rol2(UINT x, int y)
{
    return (x<<y)|(x>>(32-y));
}

32라는 숫자가 보기 싫으면 sizeof 등을 써서 다른 방식으로 바꾸면 되고.
그리고 이렇게만 짜도 컴파일러는 이 연산 전체의 의미를 알아보고 당연히 rol이라는 '비트 왼쪽 회전'이라는 '한 인스트럭션'으로 최적화해서 번역해 준다. bit shift인 shl, shr만큼이나 rotation도 굉장히 기계 친화적인 동작이며, 전용 명령이 있는 것이다. 하지만 정작 저 공개 라이브러리 함수는 Visual C++ 컴파일러가 rol이라고 최적화하지 않는다.

아마 -n shift는.. 전체 비트수에 대한 보수(32-n)만치 shift하는 것과 같다고 전제를 한 듯하다.
그리고 or 대신 xor을 쓴 것은 그게 컴퓨터 구조 차원에서 기계어 코드 길이가 더 짧거나 속도가 조금이라도 더 빨라서 그런 듯하다. 필요하다면 x=0조차도 x^=x로 표현하는 게 컴퓨터 세계이니 말이다.

결국은 음수 처리까지 정확하게 해서 shift든 rotation이든 -n만치 하는 건 반대편으로 n만치 하는 것과 같은 게 보장되는 함수를 만들려면..
if문을 써서 처리를 완전히 따로 하고 <<, >> 자체에는 어떤 경우든 음수 shift가 존재하지 않게 하는 게 이식성 면에서 가장 좋은 해결책으로 보인다. 흥미진진한 경험을 한 날이었다.

Posted by 사무엘

2014/06/15 08:36 2014/06/15 08:36
, ,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/974

1. 특정 명칭(클래스, 함수, 변수 등등)의 선언지로 곧바로 찾아가기.
(1) 소스 코드에서 cursor나 마우스 포인터가 가리키고 있는 명칭에 대해서는 현재 소속되어 있는 클래스나 namespace 문맥을 감지하여 동작해야 하며, (2) 그냥 임의의 심벌을 타이핑하여 조회하는 기능도 있어야 한다. 둘 다 필요하다.

2. 디렉터리를 불문하고 프로젝트에 있는 특정 파일 이름을 곧바로 타이핑으로 조회하여 파일 열기. 시작하는 단어와 중간에 있는 단어가 모두 지원되어야 한다.

3. 그리고 명칭이 아닌 임의의 문자열을 검색하는 Find in files인데, 다음과 같은 범위에서 모두 가능할 것.
(a) 소스(=번역 단위)든 헤더든 프로젝트에 정식으로 등록돼 있는 파일
(b) 프로젝트에 정식으로 등록은 안 돼 있지만, 등록된 파일로부터 인클루드에 의해 한 번이라도 엮이는 파일들
(c) 프로젝트 파일이 하나라도 존재하는 디렉터리에 덩달아 있는 모든 소스 파일들

즉, 3은 파일 내부의 문자열 검색이고 2는 파일 이름 자체의 검색이다. 2의 경우 일단은 검색 도메인이 (a)만으로 한정이지만, 2도 (b)나 (c)가 옵션에 따라 지원된다면 금상첨화다.

Visual Studio IDE의 경우, 1은 진작부터 인텔리센스 엔진을 통해 지원되어 왔다. 그러나 2는 2012에 와서야 가능해졌으며, 3은 (a)만 가능하다. (c)를 하려면 결국 프로젝트 경로를 수동으로 직접 입력해야만 가능하여 매우 불편함. 프로젝트에 존재하지는 않지만 같은 디렉터리에 있는 파일들을 덩달아 찾아야 할 때도 있는데도 말이다.

물론 (b)는 소스 코드를 컴파일까지는 아니어도 전처리기 수준의 파싱은 해야 구현 가능하기 때문에, 좀 어려울지 모른다. #include를 제대로 처리하려면 프로젝트 차원의 인클루드 디렉터리 관리자가 있어야 하며, 조건부 컴파일뿐만 아니라 인클루드 대상 자체에 대해서도 매크로 상수 전개가 필요할 때가 있으니 말이다.

c/cpp 같은 소스 코드가 그 자체로 온전한 번역 단위를 구성하는 게 아니라, 다른 소스 코드에 또 인클루드되어 쓰이는 경우가 있다. 물론 프로젝트에 등록되지 않은 채로 말이다.
이런 파일은 (a) 형태의 문자열이나 파일명 검색이 되지도 않아 대단히 불편하며, IDE가 구문 분석을 하는 것도 굉장히 복잡하고 어렵게 만든다. C/C++에서 인클루드는 정말 양날 달린 검인 게 실감이 간다.

끝으로 (b)와 관련된 여담 하나 좀 남기겠다.
과거 비주얼 C++ 6 시절엔 프로젝트 파일 리스트에 External dependencies라고 해서, 정식으로 프로젝트에 포함돼 있지는 않지만 프로젝트 파일에 의해 인클루드되는 파일을 대충, 얼추 계산해서 표시해 주는 기능이 있었다. '대충, 얼추'라는 말은 그 동작이 100% 정확하지는 않았다는 뜻이다. 그러던 것이 닷넷으로 넘어가면서 이 얼렁뚱땅 불완전한 기능은 삭제되었다.

그 뒤, 버전이 201x으로 넘어가면서 이 기능은 부활했다. 온전한 컴파일러가 소스 코드를 머리부터 발끝까지 다 분석하면서, MFC와 플랫폼 SDK가 중첩 인클루드하는 수십, 수백 개의 헤더 파일들을 하나도 빠짐없이 정확하게 나열해 주는 무시무시한 기능으로 다시 태어난 것이다. 비주얼 C++ IDE는 변화가 없는 것 같아도 내부적으로 이렇게 변모하고 있다.
모든 파일들의 의존도 정보를 파악하고 있다는 소리이니, 이를 바탕으로 함수 호출 tree처럼 파일들의 include 계층 다이어그램(includes / included by)을 그려 주는 기능은 IDE에 혹시 없나 궁금하다.

Posted by 사무엘

2014/04/21 08:28 2014/04/21 08:28
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/954

#define의 대체제

확실히 #define은 다른 걸로 대체 가능할 때는 가능한 한 안 쓰는 게 좋을 것 같다.
C++은 용도별로 다음과 같은 다양한 대체제를 제공한다.

1. 매크로 함수의 대체제: 인라인 함수로 대체 가능하며, 템플릿까지 동원하면 매크로 함수 만만찮은 유연한 메타프로그래밍이 가능하다.
또한 한 함수 안에서만 지엽적으로 반복되는 루틴을 정리하려면 C++0x부터는 람다 함수를 쓸 수도 있다.

2. 매크로 상수의 대체제: 정수의 경우 enum을 쓰면 같은 성격의 여러 심벌들을 한데 묶어 놓을 수도 있어서 좋다.
그리고 문자열은 그냥 const char/WCHAR 형태의 전역/클래스 static 변수로 처리함. 선언과 정의가 따로 존재해야 해서 불편할 수 있으나, 이것은 선언부에다 값을 다 집어넣고 확장 문법인 __declspec(selectany) extern const 를 지정해서 해결할 수도 있다.

아무 통제도 없이 너무 일방적으로 효력이 나타나는 #define보다는 저런 대체제들이 type-safety와 엄격한 scope 검증이 보장되기 때문에 "훨씬 더" 깔끔하다. 가능한 한 전처리기보다는 컴파일러에게 일을 맡기는 게 바람직하다.
내가 만든 명칭이 매크로로 이미 존재하여 딴 걸로 치환되고 있는 줄도 모르고 컴파일러가 자꾸 이상한 난독증을 보이며 에러를 뱉는 것 때문에 빡친 경험이 있는 사람.. 주변에 의외로 많다. ㅎㅎ

단, 그럼에도 불구하고 대체제가 존재하지 않아서 #define을 불가피하게 써야만 하는 경우는 아마도 다음과 같을 것이다.

1. #if #elif #endif 같은 조건부 컴파일 변수 지정

2. 함수 형태를 갖추기조차 민망할 정도로 너무 간단한 로직. 디버그 빌드에서도 독립된 함수 호출이 아니라 언제나 인라이닝이 반드시 보장되기를 바라는 부분

3. 호출하는 함수나 지정하는 변수 이름을 말 그대로 간단히 치환만 시키기를 원하는 경우

4. 대체제의 문법적 한도를 넘는 과격한 구문 치환을 해야 하는 경우. 특히 #나 ## 같은 연산자를 동원해서 완전히 새로운 토큰을 만들어 내야 할 때

5. __LINE__, __FILE__, __TIME__ 같은 빌드/디버그 정보를 그때 그때 삽입하고 싶을 때

6. 정수와는 달리 부동소숫점과 문자열은 여전히 #define이 유용한 경우가 있다.
부동소숫점은 enum이 지원되지 않고 static const 멤버도 클래스 선언부에서 바로 값 지정이 되지 않기 때문이다. (이걸 지원하는 컴파일러도 있긴 하나, 일단은 비표준임)
문자열은 매크로 상수의 경우, concatenate(연결)되는 문자열의 일부가 되는 게 가능하다. const 상수는 그렇지 않다.

#include와 #define이 너무 지저분하고 컴파일 시간을 증가시키는 요인이라며 없애자니.. 위와 같은 용도까지 부정하는 건 현실적으로 무리이긴 하다.

여담으로..
근래엔 남이 만든 코드를 읽다가 IID_PPV_ARGS라는 매크로를 보고 감탄하여 내가 짠 기존 코드에다가도 다 리팩터링을 해서 적용해 놨다.

CoCreateInstance와 IUknown::QueryInterface 때 꼴도 보기 싫던 void ** 형변환을 없애 주는 매우 편리하고 유용한 물건이다. COM이 등장한 건 무려 20년이 넘었고 C++에 템플릿이 추가된 것도 만만찮게 오래 됐을 텐데 이 매크로는 무려 Windows 7의 플랫폼 SDK에서야 정식 등장했다는 게 놀랍다.
매개변수 2개를 하나로 줄이는 역할까지 하니 이 정도라면 컴파일러가 아니라 전처리기 매크로밖에 선택의 여지가 없긴 하다.

Posted by 사무엘

2014/04/01 19:20 2014/04/01 19:20
,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/947

자고로 프로그래밍 언어는 구문이나 예약어 같은 원론적인 것만 정의하는 게 아니라, 그 문법을 토대로 프로그램의 작성에 필요한 각종 기본적인 자료구조와 알고리즘, 입출력 기능들도 라이브러리 형태로 제공한다. 후자의 디자인도 프로그래밍 언어의 정체성에서 차지하는 비중이 매우 크다.

예를 들어 printf, qsort, malloc, fopen 같은 함수를 사용했다면 그 함수들의 몸체도 당연히 프로그램 어딘가에 빌드되어 들어가야 한다. 아니, 애초에 main 함수나 WinMain 함수가 호출되고 각종 인자를 전해 주는 것조차도 그냥 되는 일이 아니다. 이런 것은 C/C++ 언어가 제공하는 런타임 라이브러리가 해 주는 일이며, 우리가 빌드하는 프로그램에 어떤 형태로든 코드가 링크되어 들어간다.

C/C++은 그나마 기계어 코드를 생성하는 언어이기 때문에 그런 런타임의 오버헤드가 작은 편이다. 그러나 비주얼 베이직(닷넷 이전의 클래식)이라든가 델파이는 RAD 툴이고 언어가 직접 GUI까지 다 커버하다 보니 언어가 제공하는 런타임 오버헤드가 크다.
자바나 C#으로 넘어가면 런타임이 단순 코드 오버헤드로 감당 가능한 정도가 아니라 아예 가상 기계 수준이다. 프로그램이 기계어 코드로 번역되지도 않으며, garbage collector까지 있다.

그렇게 프로그래머의 편의를 많이 봐 주는 언어들에 비해 '작은 언어'를 추구하는 C/C++은 도스나 16비트 윈도 시절에는 런타임 라이브러리가 static 링크되는 게 당연시되곤 했다. 오버헤드 자체가 수천~만 몇천 바이트밖에 되지 않을 정도로 매우 작은 편이고, 저수준 언어인 C/C++은 당연히 standalone 바이너리를 만들어야지 무슨 다른 고급 언어처럼 런타임 EXE/DLL에 의존하는 바이너리를 생성한다는 건 좀 안 어울려 보이는 게 사실이었다.

그래서 C/C++로 개발된 EXE 파일은 내부를 들여다보면 대체로, 링크되어 들어간 런타임 라이브러리의 이름과 개발사를 나타내는 문자열이 들어있었다. 볼랜드나 마이크로소프트 따위. 그래서 이 프로그램이 어느 컴파일러로 만들어졌는지를 얼추 짐작도 할 수 있었다.

C 런타임 라이브러리도 static이 아닌 DLL 형태로 제공하려는 발상은 Windows의 경우 아무래도 32비트 NT의 개발과 함께 시작된 듯하다. 그래서 윈도 NT 3.1의 바이너리를 차용해서 개발되었다는 과거의 Win32s를 보면 crtdll.dll이라는 파일이 있는데 이것이 운영체제가 기본 제공하는 프로그램들이 공용하는 C 런타임 DLL인 듯하다. 즉, 메모장, 문서 작성기, 그림판 등의 프로그램들이 호출하는 sprintf 같은 C 함수들의 구현체가 거기에 담겨 있다는 뜻이다.

재미있게도 윈도 NT의 경우, kernel32보다도 먼저 로딩되는 ntdll.dll이 내부적으로 또 각종 C 함수들의 구현체를 제공한다. 커널이 제대로 로딩되기도 전에 실행되는 프로그램이 공유하는 C 함수들이라고 한다.

과거의 윈도 9x의 프로그램들은 비록 32비트이지만 운영체제가 자체적으로 공유하는 C 런타임 DLL은 없다.
다만, 윈도 NT 3.x 이후로 비주얼 C++이 32비트 개발툴로 자리매김하면서 이 개발툴의 버전에 따라 msvcrt20.dll, msvcrt40.dll이 제공되기 시작했고 이들은 윈도 9x에서도 기본 내장되었다. 비록 같은 C 런타임이지만 버전별로 미묘한 차이가 있는 모양이다.

그러다가 1996년에 출시된 비주얼 C++ 4.2부터 C 런타임 DLL은 더 변경을 안 하려는지 파일 이름에서 버전 숫자가 빠지고, 그 이름도 유명한 msvcrt.dll이라는 이름이 정착되어 버전 6까지 쭉 이어졌다.
이 이름은 비주얼 C++ 4.2 ~ 6이 생성한 바이너리가 사용하는 C 런타임인 동시에, NT 계열에서는 기존의 crtdll을 대신하여 운영체제의 기본 제공 프로그램들이 공유하는 C 런타임의 명칭으로도 정착했다.

그리고 9x 계열도 윈도 98부터는 mfc42.dll과 더불어 msvcrt.dll을 기본 제공하기 시작했다.
NT와는 달리 운영체제가 msvcrt.dll을 직접 쓰지는 않지만, 비주얼 C++로 개발된 프로그램들을 바로 실행 가능하게 하기 위해서 편의상 제공한 것이다. 과거의 유물인 msvcrt40.dll은 msvcrt.dll로 곧바로 redirection된다.
그 무렵부터는 오피스, IE 같은 다른 MS 사의 프로그램들도 C 런타임을 msvcrt로 동적 링크하는 것이 관행으로 슬슬 정착해 나갔다.

그렇게 윈도, VC, 오피스가 똑같이 msvcrt를 사용하는 구도가 한동안 이어졌는데..
21세기에 비주얼 C++이 닷넷으로 넘어가면서 그 균형은 다시 깨졌다.
C 런타임 라이브러리도 한 번만 만들고 끝이 아니라 계속 버전업이 되어야 한 관계로 msvcr70, msvcr71 같은 DLL이 계속해서 만들어진 것이다. 결국, 비주얼 C++ 최신 버전으로 개발한 프로그램은 C 라이브러리를 동적 링크할 경우, DLL 파일을 배포해야 하는 문제를 새로 떠안게 되었다.

이것이 비주얼 C++ 2005/2008에서는 더욱 복잡해졌다. C 라이브러리를 side-by-side assembly 방식으로 배포하는 것만 허용했기 때문이다. 쉽게 말해, 레거시 공용 컨트롤과 윈도 XP의 비주얼 스타일이 적용된 공용 컨트롤(comctl32.dll)을 구분할 때 쓰이는 그 기술을 채택했다는 뜻이다.

그래서 msvcr80/msvcr90.dll은 윈도 시스템 디렉터리에만 달랑 넣는다고 프로그램이 실행되지 않는다. 이들 DLL은 DLLMain 함수에서 자신이 로딩된 방식을 체크하여 이상한 점이 있으면 고의로 false를 되돌린다. 그렇기 때문에 이들은 반드시 Visual C++ 재배포 런타임 패키지를 통해 정식으로 설치되어야 한다.

런타임 DLL간의 버전 충돌을 막기 위한 조치라고 하나 이것은 한편으로 너무 불편하고 번거로운 조치였다. C 라이브러리가 좀 업데이트돼 봤자 메이저도 아니고 마이너 버전끼리 뭐가 그렇게 버전 충돌이 있을 거라고..;; 나중에는 여러 프로그램들을 설치하다 보면 같은 비주얼 C++ 2005나 2008끼리도 빌드 넘버가 다른 놈들의 재배포 패키지가 설치된 게 막 난립하는 걸 볼 수 있을 정도였다. 가관이 따로 없다. 당장 내 컴에 있는 것만 해도 2008 기준으로 9.0.32729까지는 똑같은데 마지막 숫자가 4148, 6161, 17, 4974... 무려 네 개나 있다.

개발자들로부터도 불편하다고 원성이 빗발쳤다. MS Office나 Visual Studio급으로 수십 개의 모듈로 개발된 초대형 소프트웨어를 개발하는 게 아니라면, 꼬우면 그냥 C 라이브러리를 static 링크해서 쓰라는 소리다.
그래서 비주얼 C++ 2010부터는 C 라이브러리 DLL은 다시 윈도 시스템 디렉터리에다가만 달랑 집어넣는 형태로 되돌아갔다. 다시 옛날의 msvcrt20, msvrt40, msvcr71처럼 됐다는 뜻이다.

윈도 비스타 타임라인부터는 운영체제, VC, 오피스 사이의 관계가 뭔가 규칙성이 있게 바뀌었다.
오피스는 언제나 최신 비주얼 C++ 컴파일러가 제공하는 C 라이브러리 DLL을 사용하며, 운영체제가 사용하는 msvcrt도 이름만 그럴 뿐 사실상 직전의 최신 비주얼 C++가 사용하던 C 라이브러리 DLL과 거의 같다.

그래서 오피스 2007은 msvcr80을 사용하며, 오피스 2010은 비주얼 C++ 2008에 맞춰진 msvcr90을 사용한다. C 런타임 DLL도 꾸준히 버전업되고 바뀌는 물건이라는 것을 드디어 의식한 것이다. 비스타 이전에 윈도 2000/XP의 EXE/DLL들은 헤더에 기록된 링커 버전을 보면, 서로 사용하는 컴파일러가 다르기라도 했는지 통상적인 비주얼 C++이 생성하는 EXE/DLL에 기록되는 버전과 일치하지 않았었다. 9x는 더 말할 필요도 없고.

그럼에도 불구하고 msvcrt는 운영체제 내부 내지 디바이스 드라이버만이 사용하고, 비주얼 C++로 개발된 여타 응용 프로그램들은 언제나 msvcr##을 사용하게 용도가 확실하게 이원화되었다.
그래서 심지어는 똑같이 MS에서 개발한 한글 IME임에도 불구하고 Windows\IME 디렉터리에 있는 운영체제 보급용은 msvcrt를 사용하고, 한글 MS Office가 공급하는 IME는 Program Files에 있고 msvcr##을 사용한다. 둘은 거의 똑같은 프로그램인데도 말이다.

이것이 Windows와 C 런타임 라이브러리 DLL에 얽힌 복잡한 역사이다. 여담이지만 C 라이브러리 중에서 VC++ 2003이 제공한 msvcr71은 Windows 95를 지원한 마지막 버전이다. 2005가 제공한 msvcr80부터는 일부 보안 관련 코드에서 IsDebuggerPresent라는 API 함수를 곧장 사용함으로써 Windows 95에 대한 지원을 중단하였으며, 최하 98은 돼야 동작한다. VC++ 2008부터는 9x 계열에 대한 지원 자체가 중단됐고 말이다.

자, 여기서 질문이 생긴다. 그럼 C++은 사정이 어떠할까?

C보다 생산성이 훨씬 더 뛰어난 C++로 주류 프로그래밍 언어가 바뀌면서 표준 C++ 라이브러리에 대한 동적 링크의 필요성도 응당 제기되었다. 그러나 이것은 C 라이브러리보다는 시기적으로 훨씬 더 늦게 진행되었기 때문에 가장 먼저 등장하는 비주얼 C++의 DLL 이름이 msvcp50과 msvcp60이다. 즉, 비주얼 C++ 5.0 이전에는 선택의 여지 없이 static 링크만 있었다는 뜻이다.

더구나 이것은 전적으로 비주얼 C++ 소속이지, 운영체제가 따로 C++ 라이브러리 DLL을 제공하지는 않는다. MS에서 만들어진 제품들 중에 msvcp##를 사용하는 물건은 비주얼 C++ IDE와 컴파일러 그 자신밖에 없다.

C++ 라이브러리는 대부분 템플릿 형태이기 때문에 알고리즘 뼈대는 내 바이너리에 static 링크되는 게 사실이다. 그러나 그렇게 덧붙여지는 라이브러리 코드가 별도로 호출하는 함수 중에 DLL에 의존도를 지니는 게 있다. cout 객체 하나만 사용해도, 그리고 STL 컨테이너 하나만 사용한 뒤 빌드해 봐도 형체를 알 수 없는 이상한 메모리 관리 쪽 함수에 대한 의존도가 생긴다.

그래도 C 라이브러리 DLL은 사용한 함수에 따라서 printf, qsort 등 링크되는 심벌이 명확한 반면
C++ 라이브러리는 내부 구조는 이거 뭐 전적으로 컴파일러가 정하기 나름이고 외부에서 이들 심벌을 불러다 쓰기란 사실상 불가능하다는 큰 차이가 있다.

<날개셋> 한글 입력기는 C++ 라이브러리를 사용하지 않으며, 빌드 후에 생성된 바이너리를 인위로 수정하여 msvcr## 대신 강제로 운영체제의 msvcrt를 사용해서 동작하게 만들어져 있다.

Posted by 사무엘

2013/11/19 08:29 2013/11/19 08:29
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/900

지뢰찾기 연구

요즘 팔자에도 없던 지뢰찾기에 살짝 재미가 붙었다.
본인은 비슷한 학력· 경력으로 IT 업계에 종사하는 여느 사람들과는 달리, 머리 싸움을 즐기는 스타일이 전혀 아니었으며 복잡한 퍼즐 게임 따위와도 담을 쌓고 지내는 편이었다. 이런 점에서 본인은 완전 퍼즐 게임 매니아인 T모 님과는 성향이 다르다.

그래도 지뢰찾기 정도면 요령을 알고 나니까 은근히 재미있다. 초보 레벨로는(9*9, 지뢰 10개) 40초~1분 남짓한 시간 동안 대략 60~70%대의 승률로 깨겠다. 처음엔 초보 레벨조차도 5분이 넘게 끙끙대기도 했으나, 마치 경부선 서울-부산 열차의 운행 시간이 17시간에서 6시간대~4시간대로 줄어들듯이 시간이 단축되었다.

사용자 삽입 이미지
지뢰찾기는 소련에서 개발된 테트리스와 더불어 시간 죽이기용으로 상당히 적절한 컴퓨터용 퍼즐인 거 같다. 여느 보드 게임과는 달리, 물건이 먼저 존재하다가 컴퓨터로 옮겨진 게 아니라 처음부터 컴퓨터용으로 만들어진 게임이라는 차이가 있다.

맥북의 터치패드 격인 트랙패드로는 도저히 게임을 할 수 없는 듯했다.
두 손가락을 동시에 누르거나 패드 우측 하단을 지그시 누르면 우클릭이 되긴 하는데, 이게 생각보다 정확하게 인식되지가 않는 듯하기 때문이었다.

지뢰가 있다는 깃발만 꽂으려고 우클릭을 했는데, 그게 좌클릭으로 인식되어 지뢰를 밟고 장렬히 죽는 참사가 한두 번 발생하는 게 아니어서 말이다. 단, Windows Vista 이후부터 새로 개발된 지뢰찾기는 Shift+클릭으로 우클릭, 더블클릭으로 좌우 클릭도 돼서 조작이 훨씬 더 편해졌다.

키보드로는 Space는 셀 개봉(좌클릭)이고, Shift+Space가 깃발(우클릭)이다.
그런데 이번엔 깃발이 꽂힌 것을 제외한 모든 인접 셀들을 한꺼번에 개봉하는 건 키보드로 어떻게 하는지 모르겠다. 게임에 익숙해지고 나면 셀 개봉은 하나씩 클릭하는 것보다 저렇게 개봉을 훨씬 더 즐겨 하게 되는데 말이다.

지뢰찾기라는 게임은 풀이 순서를 논리적으로 명확하게 유추 가능한 상황이 대부분이지만, 가끔은 주어진 정보만으로는 정확한 지뢰 배치를 알 수 없어서 찍기(guessing)를 해야 하는 경우도 있다. 지뢰가 정확하게 어떤 조건으로 배치되어 있을 때 그런 상황이 생기는지는 잘 모르겠다.

숫자 정보로부터 유추 가능한 지뢰 배치 가짓수는 기본적으로 폭발적으로 증가할 수 있으며, 어떻게 될 수 있는지 백트래킹으로 일일이 하나하나 때려박아 넣으며 추적을 하는 수밖에 없다. 뭔가 네모네모 로직을 푸는 것 같은 느낌이 들기도 한다. 이 때문인지 이 문제는 전산학적으로 봤을 때 NP 완전 문제라는 것까지 증명되었다.

그리고 찍기가 필요 없는 명확한 상황일 때 사람이 지뢰를 찾는 절차는 의외로 아주 명료하고 기계적이다.
딱 이 정도 영역이 개봉되고 인접 셀의 지뢰 정보가 이렇게 주어졌을 때, '명백한 해법' 하나만 동원해서라도 컴퓨터가 게임 진행을 충분히 도와 줄 수 있겠다는 생각이 들었다.

그래서, 막간을 이용해 지뢰찾기를 푸는 프로그램을 짜 봤다.
초-중급 수준의 간단한 클래스 설계와 알고리즘 구현이 동원되니 심심풀이 땅콩 코딩용으로 꽤 적절한 거 같다!
'명백한 해법'을 적용할 수 없어서 '찍기'를 해야 할 때, 지뢰가 있을 만한 위치를 가장 유력하게 유추하는 것 정도까지 구현해야 비로소 중급-고급 사이를 넘볼 수 있지 싶다.

대략의 코딩 내역은 이러하다.
지뢰 답을 알고 있는 MineSource 클래스(각 칸마다 지뢰 여부를 실제로 담고 있는 2차원 배열),
그리고 그 MineSource에다가 쿼리를 해 가며 1~8 숫자와 자기가 찾아낸 지뢰 위치 정보만을 알고 있는 MineSolver 클래스를 만들었다.
이들은 다 2차원 배열과 배열의 크기는 공통 정보로 갖고 있으므로 MapData라는 동일 기반 클래스를 설정했다.

MineSource는 특정 위치 x,y에 대한 쿼리가 온 경우, MineSolver에다가 인접 셀들의 지뢰 개수를 써 준다. 인접 셀에 지뢰가 하나도 없다면 여느 지뢰찾기 프로그램이 그러는 것처럼 인접 셀 8개도 한꺼번에 개봉하면서 flood fill을 한다.
곧바로 지뢰를 찍었다면 당연히 곧바로 게임 오버라고 알려 준다. 그리고 요즘 지뢰찾기 게임 구현체들이 그런 것처럼, 첫 턴에서는 절대로 지뢰를 찍는 일이 없게 내부 보정을 하는 것도 이 클래스에서 하는 일이다.

지뢰찾기의 '명백한 해법'은 딱 두 가지이다.

  1. 열리지 않은(지뢰 마크가 있는 놈 포함) 인접 셀의 개수와 자기 숫자가 '같은' 셀은 주변 미개봉 셀이 다 지뢰임이 100% 확실하므로 그것들을 전부 지뢰 마크(깃발)로 표시한다.
  2. 깃발이 꽂힌 주변 셀의 개수와 자기 숫자가 같은 셀의 경우, 지뢰 마크가 없는 나머지 열리지 않은 인접 셀은 지뢰가 아닌 게 100% 확실하다. 따라서 전부 개봉한다.
  3. (위의 명백한 해법만으로 개봉할 만한 셀이 존재하지 않는 건 운이 나쁜 케이스다. 패턴을 기반으로 랜덤 추측을 해야 하는데, 이건 일단 보류.)

텍스트 모드에서 자기 스스로 무작위하게 지뢰밭을 만들고 지뢰찾기를 풀기도 하는 자문자답 프로그램을 만드니, 200줄이 좀 안 되는 코드가 완성되었다.
이 프로그램은 인접 셀에 대해서 뭔가 조건을 만족하는 셀의 개수를 세거나, (getCount) 일괄적으로 동일한 조치를 취하는(doAction) 패턴이 많다.

이걸 그냥 for(j=y-1; j<=y+1; j++) for(i=x-1; i<=x+1; i++)이라는 2중 for문만으로 돌리기에는 i나 j가 boundary 밖인 경우도 고려해야 하고, (i,j)가 (x,y)와 같은 위치인 경우도 피해 가야 하기 때문에 loop 자체가 생각보다 복잡하다.
그러니, 그 loop 자체만 하나만 짜 놓고 loop 안에서 하는 일을 그때 그때 달리 지정하는 것은 템플릿-람다로 깔끔하게 설계했다.

다음은 프로그램의 간단한 실행 결과이다.

after the first turn:
+ + 1 . . . . . .
+ + 1 . 1 1 1 . .
+ + 1 . 1 + 2 1 .
+ + 1 . 1 2 + 1 .
1 1 1 . . 1 + 2 1
. . . . 1 1 + + +
. . . . 1 + + + +
. 1 1 2 2 + + + +
. 1 + + + + + + +

(중간 과정 생략)

picking 7 9
@ @ 1 . . . . . .
2 2 1 . 1 1 1 . .
1 1 1 . 1 @ 2 1 .
1 @ 1 . 1 2 @ 1 .
1 1 1 . . 1 2 2 1
. . . . 1 1 3 @ 2
. . . . 1 @ 3 @ 2
. 1 1 2 2 2 2 1 1
. 1 @ 2 @ 1 . . .
You Won!


이 정도 초보적인 지뢰 찾기 풀이 프로그램은 이미 다 개발되고도 남았으니,
유튜브를 뒤지면 신의 경지 수준의 속도를 자랑하는 지뢰찾기 TAS (매크로 프로그램 내지 역공학을 동원한 게임 스피드런) 동영상들이 나돌고 있다.

여담이다만, 지뢰찾기를 하다가 지뢰를 밟아서 게임 오버가 될 때 본인은 깜짝 깜짝 잘 놀란다. =_= 마치 옛날에 페르시아의 왕자를 하는데 타이밍을 잘못 잡아서 왕자가 쇠톱날(chopper)에 두 동강 나서 죽는 것 같은 그런 느낌이다.

Posted by 사무엘

2013/09/26 08:32 2013/09/26 08:32
, , , ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/881

3. 더 기괴하고 잉여력마저 의심되는 오버로딩

(1) 비트 연산자도 아니고 논리 연산자 && || !는 오버로딩할 일이 거의 없으며, 각종 C++ 디자인 패턴 책에서도 오버로딩을 권하지 않는 물건들이다. 그 연산자를 건드릴 게 아니라 개체를 건드리는 게 순리이다. 논리 연산자들이 취급할 수 있는 정수나 boolean 값으로 형변환하는 연산자를 제공하는 게 이치에 맞다.

굳이 논리 연산자를 오버로딩해 버리면, 일단 언어가 원래 제공하는 단축연산 기능이 사라지게 된다. 즉, A && B에서 A가 이미 false이면 B의 값은 아예 계산하지 않고 함수를 호출하지도 않는 것 말이다. 오버로딩된 함수는 논리 연산자라도 언제나 A와 B의 값을 먼저 계산한 뒤에 실행된다.

(2) 어떤 개체가 메모리에 차지하는 주소를 얻어 오는 기능은 그 어떤 타입이나 클래스에 대해서도 절대불변으로 동작해야 하는 기능이지 않은지? 마치 개체의 고정된 크기를 얻어 오는 sizeof 연산자처럼 말이다.
그럼에도 불구하고 C++의 단항 & (address-of) 연산자는 오버로딩 가능하다!

class Foo {
    //...
public
    int operator&() { return 0; }
};

void Bar(Foo *p)
{
    //...
}

이렇게 선언하거나 더 얄밉게 연산자 함수를 아예 private로 감춰 버리고 나면,
지역변수나 클래스/구조체의 멤버로 직접 선언된 Foo 개체는 Bar라는 함수에다 넘겨주는 게 불가능해진다. =_=;;;

Foo a; Bar(&a);

이런 테크닉이 무슨 필요나 의의가 있는지는 난 잘 모르겠다.

전통적인 &는 변수의 주소라는 R-value만 되돌리는 연산자인데, 이를 오버로딩하면 &는 참조자 같은 걸 되돌릴 경우 L-value를 되돌리는 것도 가능해진다. 따라서 그 값에 대해서 또 주소를 얻는 &를 적용하는 게 덩달아 가능해진다.
그러나 이 경우 &&를 연달아 쓸 수는 없으며, & &a 같은 식으로 토큰을 분리해 줘야 한다. 예전에 중첩 템플릿 인자를 닫을 때 > 사이에 공백을 넣어 줬던 것처럼 말이다.

(3) 게다가, 우선순위가 가장 낮으며 그저 여러 연산자들을 한데 나열하는 역할만을 하는 콤마 연산자도 오버로딩 가능하다! (,는 오버로딩 없이도 원래 아무 피연산자에 그 어떤 타입이 와도 무조건 괜찮은 유일한 이항 연산자임)
콤마는 함수 인자 구분용으로도 쓰인다는 특성상, 이 연산자는 가변 인자 함수 호출을 흉내 내는 용도로 쓰일 수 있을 것 같다. list, 3, 2, 1, 8, 4; 이라고 써 주고 list.add(3); list.add(2); ... 같은 효과를 낼 수도 있다는 뜻이다. 하지만 이걸 남발하는 건 좀 사악한 짓인 듯.

(4) 기괴한 오버로딩의 진정한 종결자로 내가 최후까지 남겨 둔 건 바로 ->* (pointer-to-member) 이다. 얘는 유사품인 ->하고는 오버로딩을 하는 방식이 사뭇 다르다!
-> 연산자가 아무 인자가 없는 멤버 함수인 반면, ->*는 단 하나의 인자를 받는다. 그 인자는 아무 타입이나 될 수 있으며, ->* 연산자 함수 자체도 다양한 타입으로 오버로딩될 수 있다. 가령,

POINT& operator->*(int x) { return m_pt[x]; }

이렇게 오버로딩이 된 클래스가 있다면

(obj->*0).x = 100;

이런 식으로 활용이 가능하다. 0이 연산자 함수의 인자로 전달된다. 0뿐만이 아니라 당연히 int 변수 n 같은 것도 줄 수 있다. .이나 -> 연산자 다음에는 구조체/클래스의 멤버가 뒤따라야 하는 반면, .*이나 ->* 연산자 다음에는 임의의 타입에 속하는 value가 올 수 있는 구조인 것이다. ->는 가리키는 대상 포인터이지만 .*는 대상으로부터 얻을 오프셋 자체가 고정이 아니라 동적이며, ->*는 대상과 오프셋이 모두 동적임을 뜻한다.

struct A { int x,y; };

struct B { A m_Obj; };

이렇게 A를 멤버로 갖는 B라는 클래스가 있다고 치자.
클래스의 멤버 포인터는 클래스에 종속적이다.
그러므로 클래스 B에 대해서 A에 소속된 멤버 포인터를 적용하고 싶다면 ->* 연산자를 오버로딩하여 다음과 같은 연산자 함수를 써 주면 된다.

int& operator->*(int A::*t) { return m_Obj.*t; }

그러면

B bar;
int A::*temp = &A::x;

bar->*temp = 100;
bar.m_Obj.*temp = 100;

위의 두 구문은

bar.m_Obj.x = 100;

과 동일한 의미를 지니게 된다. 실무에서 이걸 오버로딩할 일이 있을지는 잘 모르겠지만..;;
멤버 변수가 저렇고, 멤버 함수의 포인터에 대해서는 머리가 터질 것 같아서 생략하련다.
C++의 세계가 더욱 심오하게 느껴지지 않는가?

4. C++ 연산자 오버로딩의 한계

(1) 당연한 말이지만 원래 C++ 언어에 없는 새로운 토큰을 만들어 낼 수는 없다. 가령, @, ** 같은 듣보잡 기호를 연산자로 정의할 수는 없다. 특히 *는 포인터의 연쇄 역참조용으로도 쓰이기 때문에 ** 같은 건 C++에서 절대로 토큰으로 쓰일 수 없는 문자열이다.

(2) .(구조체 멤버 참조) .*(멤버 포인터) ::(scope) ?:(조건 판단. 유일한 삼항 연산자) sizeof 연산자는 의미가 완전히 고정되어 있으며 재정의할 수 없다.

(3) C/C++이 원래 정의하고 있는 연산자의 우선순위와 피연산자 결합 방향을 변경할 수는 없다. 그리고 built-in type에 대해 이미 정의되어 있는 연산의 의미를 재정의할 수도 없다.

이런 모든 구체적인 디테일들을 다 명시해야 한다면 C++의 참고용 매뉴얼은 정말 상상을 초월하게 두꺼울 수밖에 없겠다는 게 실감이 간다.

Posted by 사무엘

2013/07/17 08:36 2013/07/17 08:36
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/856

오늘은 C++에서 기초적이면서도 아주 심오하고 기괴한 문법 중 하나인 연산자 오버로딩에 대해서 좀 살펴보도록 하자.
정수, 포인터 같은 기본 자료형뿐만 아니라 사용자가 만든 임의의 개체도 연산자를 통해 조작할 수 있으면 천편일률적인 함수 호출 형태보다 코드가 더 깔끔하고 보기 좋아지는 효과가 있다. 물론, 아무 논리적 개연성 없이 오버로딩을 남발하면 코드를 알아보기 힘들어지겠지만 말이다.

C++은 연산자 오버로딩을 위해 operator라는 키워드를 제공한다. 그리고 개체에 대한 연산자 적용은 연산자 함수를 호출하는 것과 문법상 동치이다(syntactic sugar). 가령, a+b는 a.operator +(b) 또는 ::operator +(a,b)와 동치라는 뜻이다. 연산자 함수는 표준 규격이 없이 C++ 컴파일러마다 제각각으로 제정한 방식으로 심벌 명칭이 인코딩된다.

앞에서 예를 들었듯이 연산자 함수는 클래스에 소속될 수도 있고, 특정 클래스에 소속되지 않은 중립적인 전역 함수로 선언될 수도 있다. 클래스의 멤버 함수인 경우 this가 선행 피연산자에 포함된 것으로 간주되어 함수의 인자 개수가 하나 줄어든다. 연산자 함수는 명백한 특성상 default 인자를 갖는 게 허용되지 않는다.

한 클래스와 관련된 처리는 당연히 가능한 한 클래스의 내부에서 다 알아서 하는 게 좋다. 그러니 전역 함수 형태의 연산자 함수는 이항 연산자에서 우리 타입이 앞이 아니라 뒤에 나올 때 정도에나 필요하다. 가령, obj+1이 아니라 1+obj 같은 형태일 때 말이다. 그리고 이 전역 함수는 당연히 클래스의 내부에 접근이 가능해야 연산을 수행할 수 있을 것이므로 보통 friend 속성이 붙는다.

연산자가 클래스 함수와 전역 함수 형태로 모두 존재할 경우엔 모호성 에러가 나는 게 아니라 전역 함수가 우선적으로 선택되는 듯하다. a+b에 대해서 ::operator +(a,b)와 a.operator +(b)가 모두 존재하면 전자가 선택된다는 뜻. 언어의 설계가 원래 그렇게 된 건지는 잘 모르겠다.

본인은 연산자 오버로딩을 다음과 같은 양상으로 분류해 보았다.

1. 쉽고 직관적인 오버로딩

(1) 사칙연산, 대입, 비교, 형변환 정도가 여기에 속한다. 기본 중의 기본이다.
사칙연산을 재정의함으로써 C++에서도 문자열을 +를 써서 합치는 게 가능해졌다(동적 메모리 관리까지 하면서). 그리고 복소수, 유리수, 벡터 같은 복합적인 수를 표현하는 자료형을 만들 수 있게 되었다.

(2) 내부적으로 포인터 같은 외부 리소스를 참조하고 있기 때문에 단순 memcpy, memcmp 알고리즘으로는 대입이나 비교를 할 수 없는 클래스의 경우, 해당 연산자의 오버로딩이 필수이다. 이 역시 문자열 클래스가 좋은 예이다. 대입 연산자는 복사 생성자와도 비슷한 구석이 있다.
대입 연산자는 생성자와 역할이 비슷하다는 특성상, 연산자들 중 상속이 되지 않는다. 모든 클래스들은 상속에 의존하지 말고 자신만의 대입 연산자를 갖추고 있어야 한다는 뜻이다. 단, 순수 대입인 = 말고 +=, -= 같은 부류들은 상속됨.

(3) 비교 연산은 같은 클래스에 속하는 여러 원소들을 다른 컨테이너 클래스에다 집어넣어서 순서대로 나열해야 할 때 매우 요긴하게 쓰인다. 전통적인 C 스타일의 비교 함수보다 훨씬 더 직관적이다.

(4) 형변환 연산자야 상황에 따라 어떤 개체의 형태를 카멜레온처럼 바꿔 주고 개체의 사용 호환성을 높여 주는 기능이다. 포인터나 핸들 하나로만 달랑 표시되는 자료형에 대해서 자동으로 메모리 관리를 하고 여러 편의 기능을 수행하는 wrapper 클래스를 만든다면, 본디 자료형과 나 자신 사이의 호환성을 이런 형변환 연산자로 표현할 수 있다.
문자열 클래스는 const char/wchar_t * 같은 재래식 문자 포인터로 자신을 자동 변환하는 기능을 제공하며, MFC의 CWnd도 이런 맥락에서 HWND 형변환을 지원하는 것이다.

(5) 한편, 비트 연산자는 산술 연산보다야 쓰이는 빈도가 아무래도 낮다. 비트 벡터나 집합 같은 걸 표현하는 클래스를 만드는 게 아닌 이상 말이다.
단, 뭔가 직렬화 기능을 제공하는 스트림 개체들이 << >>를 오버로딩한 것은 굉장히 적절한 선택인 것 같다. MFC의 CArchive라든가 C++의 iostream 말이다. input, output이라는 방향성을 표현할 수 있고 또 비트 shift라는 개념 자체가 뭔가 '이동, 입출력'이라는 심상과 잘 맞아떨어지기 때문이다.

다른 연산자들보다 적당히 우선순위가 낮은 것도 장점. 하지만 산술 연산 말고 비교나 비트 연산자는 <<, >>보다도 우선순위가 낮으므로 사용할 때 유의할 필요가 있다.

2. 아까 것보다는 좀 더 생소하고 어려운 오버로딩

(1) 필요한 경우, operator new와 operator delete를 오버로딩할 수 있다. 물론, 객체를 생성하고 소멸하는 new/delete 본연의 기능을 완전히 뒤바꿀 수는 없지만, 이 연산자가 하는 일 중에 메모리를 할당하여 포인터를 되돌리는 기능 정도는 바꿔치기가 가능하다는 뜻이다. C/C++ 라이브러리가 기본 제공하는 heap이 아닌 특정 메모리 할당 함수를 써서 특정 메모리 위치에다가 객체를 배체하고 싶을 때 말이다.

특히 new/delete는 여러 개의 인자를 받을 수 있는 유일한 연산자 함수이다. size_t 형태로 정의되는 메모리 크기 말고 다른 인자도 받는 다양한 버전을 만들어서 new(arg) Object; 형태의 문법을 만들 수 있으며, 이때 arg는 Object의 생성자가 아니라 operator new라는 함수에다 전달된다. (delete 연산자는 어떤지 잘 모르겠다만..;;)

그리고 이놈은 전역 함수와 클래스 멤버 함수 모두 가능하다. 클래스 멤버 함수일 때는 굳이 static을 붙이지 않아도 이놈은 static이라고 간주된다. 당연히.. malloc/free 함수의 C++ 버전인데 this 포인터가 전혀 의미가 없는 문맥이기 때문이다..

사실, C++은 단일 개체를 할당하는 new/delete와 개체의 배열을 할당하는 new []/delete []도 구분되어 있다. 후자의 경우, 생성자나 소멸자 함수를 정확한 개수만치 호출해야 하기 때문에 배열 원소의 개수를 몰래 보관해 놓을 공간도 감안하여 메모리가 할당된다.
그러나 이것은 언제까지나 컴파일러가 알아서 계산을 하는 것이기 때문에 배열이냐 아니냐에 따라서 메모리 할당/해제 함수의 동작이 딱히 달라져야 할 것은 없다. 굳이 둘을 구분해야 할 필요가 있나 모르겠다.

(2) ++, --는 정수나 포인터 같은 아주 간단한 자료형과 어울리는 단항 연산자이다. 그런데 개체의 내부 상태를 한 단계 전진시킨다거나 할 때... 가령, 연결 리스트 같은 자료형에서 iterator를 다음 노드로 이동시키는 것을 표현할 때는 요런 물건이 유용하다. 난 파일 탐색 함수를 클래스로 만들면서 FindNextFile 함수 호출을 해당 클래스에 대한 ++ 연산으로 표현한 적이 있다. ㅎㅎ

++, --는 전위형(prefix ++a)과 후위형(postfix a++)이 둘 존재한다. 후위형 연산자 함수는 전위형 연산자 함수에 비해 잉여 인자 int 하나가 추가로 붙는다. 그리고 후위형 연산자는 자신의 상태는 바꾸면서 바뀌기 전의 자기 상태를 담고 있는 임시 객체를 되돌려야 하기 때문에 처리의 오버헤드가 전위형보다 더 크다. 아래의 코드를 참고하라.

A& A::operator++() //++a. a+=1과 동일하다.
{
    increment_myself();
    return *this;
}

A A::operator++(int) //a++.
{
    A temp(*this);
    increment_myself();
    return temp;
}

보다시피, 전위형과 후위형의 실제 동작 방식은 전적으로 관행에 따른 것이다. 전위형이 후위형처럼 동작하고 후위형이 전위형처럼 반대로 동작하게 하는 것도 프로그래머가 마음만 먹으면 엿장수 마음대로 얼마든지 가능하다.
정수의 경우 프로그래머가 for문 같은 데서 a++이라고 쓴다 해도 똑똑한 컴파일러가 굳이 임시 변수 안 만들고 ++a처럼 최적화를 할 수 있다. 그러나 사용자가 오버로딩한 연산자는 실제 용례가 어찌 될지 알 수 없으므로 컴파일러가 선뜻 최적화를 못 할 것이다.

일반적으로 a=a++의 실행 결과는 컴파일러의 구현 내지 최적화 방식에 따라 들쭉날쭉 달라지는 것으로 잘 알려져 있다. (당장 비주얼 C++와 xcode부터가 다르다!) 또한 a가 일반 정수일 때와, 클래스일 때도 동작이 서로 달라진다. 이식성은 완전히 안드로메다로 간다는 뜻 되겠다. 더구나 한 함수 호출의 인자 안에서 a와 a++이 동시에 존재하는 경우, 어떤 값이 들어가는지는 같은 컴파일러 안에서도 debug/release 빌드에 따라 차이가 생길 수 있으므로 이런 모호한 코드는 작성하지 않아야 한다.

(3) 포인터를 역참조하는 연산자인 * ->는 원래는 포인터가 아닌 일반 개체에 대해서 쓰일 수 없다. 그러나 C++은 이런 연산자를 오버로딩함으로써 인위적인 '포인터 역참조' 단계를 만들 수 있으며, 이로써 포인터가 아닌 개체를 마치 포인터처럼 취급할 수 있다. 형변환 연산자와 비슷한 역할을 하는 셈이다.

C++ STL의 iterator가 좋은 예이고, COM 포인터를 템플릿 형태로 감싸는 smart pointer도 .을 찍으면 자신의 고유한 함수를 호출하고, ->를 찍으면 자신이 갖고 있는 인터페이스의 함수를 곧장 호출하는 형태이다.
-> 연산자는 전역 함수가 아니라 인자가 없는 클래스 멤버 함수 형태로 딱 하나만 존재할 수 있다. 다음 토큰으로는 구조체나 클래스의 멤버 변수/함수가 와야 하기 때문에 리턴 타입은 구조체나 클래스의 포인터만이 가능하다.

(4) []도 재정의 가능하고 심지어 함수 호출을 나타내는 ()조차도 연산자의 일종으로서 재정의 가능하다!
[]의 경우 클래스가 포인터형으로의 형변환을 제공한다면 정수 인덱스의 참조는 컴파일러가 자동으로 알아서 그 포인터의 인덱스 참조로 처리한다. 그러나 [] 연산자 함수는 굳이 정수를 받지 않아도 되기 때문에 특히 hash 같은 자료구조를 만들 때 mapObject[ "H2O" ] = "산소"; 같은 구문도 가능해진다. 단, 진짜 고급 언어들처럼 인자를 여러 개 줄 수는 없다. map[2, 5] = 100; 처럼 할 수는 없다는 뜻.

()도 사정이 비슷한지라, 클래스가 함수 포인터로의 형변환을 제공한다면 굳이 () 연산자를 재정의하지 않아도 개체 다음에 곧장 ()를 붙이는 게 가능은 하다. 그러나 그 함수 포인터는 this 포인터가 없는 반면, () 연산자 함수는 this를 갖는 엄연한 멤버 함수이다.
함수 포인터로의 형변환 연산자 함수는 언어 문법 차원에서 토큰 배열의 한계 때문에 대상 타입을 typedef로 먼저 선언해서 한 단어로 만들어 놔야 한다.

Posted by 사무엘

2013/07/15 08:35 2013/07/15 08:35
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/854

1.

남이 짠 레거시 코드를 업무상 들여다보다가 우연히 발견한 건데,
아래의 코드는 비주얼 C++에서는 컴파일되지만 gcc 계열의 다른 컴파일러에서는 컴파일되지 않는다.

class A {
public:
    class B;
};

class A::B {
public:
    A::B() { }
};

일단, 클래스 내부의 하위 클래스를 저런 식으로 forward 선언했다가 나중에 다시 선언하는 문법 자체를 본인은 직접 구사한 적이 전혀 없었다.
그랬는데, 어찌된 일인지 생성자 함수를 선언할 때 클래스 A까지 다 써 주는 것을 비주얼 C++은 허용하지만 다른 컴파일러들은 그러하지 않았다.
왜 그런지, 이와 관련된 표준 규정은 없는지 궁금하다.

2.

Lyn Tono 님의 블로그를 보다가, 평소에 미처 생각도 못 했던 흥미로운 사실을 발견하여 이곳에다가도 소개하겠다.

void foo() { puts("global function"); }

template<typename T>
class C {
    T m;
public:
    //static이든 아니든 사실 상관은 없음
    static void foo() { puts("Member function"); }
};

template<typename T>
class D: public C<T> {
public:
    void bar() { foo(); }
};

위와 같은 함수와 템플릿 클래스를 선언한 뒤 D<int> q; q.bar(); 라고 실행하면,
비주얼 C++에서는 클래스에 소속된 foo 멤버 함수가 불리지만, 역시 gcc 계열의 다른 컴파일러에서는.. 놀랍게도 global 함수가 불린다!
이건 우선순위 문제도 아닌지라, global 함수를 없앤다고 해서 멤버 함수가 차선으로 지명되지도 않는다. 그 경우에도 클래스 멤버는 존재가 무시되고 그냥 컴파일 에러가 난다. -_-;;

그렇다. 템플릿 클래스는 기반 클래스의 멤버 지명이 비템플릿 클래스처럼 그렇게 쉽게 되질 않는다.
this-> (멤버. 심지어 this 포인터가 존재하지 않는 static 함수이더라도) 혹은 :: (전역)을 명시적으로 써 줘야 한다.

내가 생업을 위한 코딩을 하는데 비주얼 C++을 벗어날 일은 거의 없지만, 저 상황에서 당연히 멤버 함수가 불릴 거라고 예상한 게 다른 컴파일러에서는 그렇지 않을 수도 있다는 걸 염두에 둬야겠다.

3.

예전에 비주얼 C++이 지원하는 for each 문에 대해 소개를 했었고, 친절하게 의견을 남겨 주신 김 진 님으로부터 다른 컴파일러에도 range-based for 문에 대한 비슷한 문법이 존재한다는 보충 설명도 들었다.

그런데 비주얼 C++의 경우, for each() 내부에서 중괄호 없이 if...else문을 썼는데, else부터가 왜 인식이 안 되지? 최신 2012까지도. 아무래도 버그가 아닌지 의심된다. 아래의 예들을 보시라.

for(i=0; i<10; i++) if(a) b(); else c(); //원래 OK

for each(auto i in container) if(a) b(); else c(); //ERROR: 이것만 안 될 이유가 없잖아? 왜? 게다가 인텔리센스 컴파일러는 이를 에러로 지적하지 않음.

for each(auto i in container) { if(a) b(); else c(); } //중괄호를 해 주면 OK

for each(auto i in container) a ? b(): c(); //차라리 이렇게 하는 것도 OK

for(auto i: container) if(a) b(); else c(); //xcode의 이 문법도 당연히 아무 문제 없이 OK. 참고로 비주얼 C++도 2012부터는 for each뿐만 아니라 이 문법도 지원하기 시작했으며, else문에 이상이 없다.

웹 표준만큼이나 C++ 표준도 세밀한 부분에서의 이행 여부가 컴파일러마다 케바케인 것 같다.
옛날엔 IE6이 웹 표준을 안 지킨다고 욕 얻어먹은 것만큼이나 VC6도 C++ 표준 미준수 때문에 많은 비판을 받았었다. for 문 안에서 선언한 변수의 scope 문제가 제일 유명하고 말이다.

하지만 표준 자체가 모호하거나 아예 커버하지 않고 있는 사항이 있다면 그저 묵념..

여담이지만, 2차원 배열을 순회하는 경우 비주얼 C++의 for each는 배열 안의 각 원소를 하나씩 일일이 순회하는 반면, for(:) 문은 각 배열의 포인터를 변수에다 넘겨 주면서 여전히 1차원적으로만 순회한다는 차이도 있다.

4.

C/C++에서 작은따옴표는 문자 상수를 나타낸다. sizeof('a')의 값이 C에서와 C++에서 서로 다르다는 건 이미 잘 알려진 사실. 그런데 작은따옴표 안에 탈출문자가 아닌 일반 문자가 둘 이상 중첩되는 게 문법적으로 가능하며, 에러가 아니다. 그리고 더욱 기괴한 것은, 그렇게 중첩되었을 때 이 문자 상수가 갖는 값은 표준으로 정해져 있지 않고 컴파일러 구현체가 해석하기 마음대로라는 것! 일부러 그렇게 규격을 '미정'으로 남겨 놨다.

대부분의 컴파일러에서는 'ab'를 0x4142라고 합성해서 인식하는 식의 배려는 해 주고 있다. 그러나 이것은 애초에 표준 동작이 아니다 보니, 컴파일러의 기반 아키텍처 또는 코드 생성 대상 아키텍처의 엔디언에 따라 세부적인 동작이 달라진다. 다시 말해 이것은 이식성을 전혀 보장받을 수 없는 지뢰밭 같은 테크닉이며, 그렇기 때문에 IOCCC 같은 대회에서 써먹을 수도 없다.

그럴 거면 둘 이상의 문자는 차라리 깔끔하게 경고나 에러 처리라도 해 주지 하는 아쉬움이 있다.
<날개셋> 한글 입력기의 수식은 문자 상수로 둘 이상의 문자를 집어넣으면 문법 에러로 간주한다.

Posted by 사무엘

2013/04/25 08:38 2013/04/25 08:38
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/822

« Previous : 1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/04   »
  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        

Site Stats

Total hits:
2677295
Today:
1863
Yesterday:
2124