몇 가지 C++ 이야기

1. 람다 함수와 내부 클래스 사이의 관계

본인은 몇 년 전엔 Visual C++ 2010으로 갈아탄 기념으로, <날개셋> 한글 입력기의 소스 코드 내부에서도 한 함수 안에 임시로 사용되던 잡다한 매크로 함수들을 대부분 람다나 템플릿 함수로 바꿨다. 이건 무식한 #define의 사용을 최소화한다는 코딩 컨벤션과 부합하는 리팩터링이었다. #define은 조건부 컴파일이나 ##처럼 정말로 컴파일러의 영역을 초월하여 전처리기의 도움이 필요한 영역에서나 제한적으로 사용할 것이다.

그 당시엔 매크로가 함수 안의 지역변수들을 건드리는 건 대부분 캡처로 기계적으로 치환해서 집어넣었다. 허나 무분별한 캡처 난발도 지금 보니 무척 지저분하게 보인다는 것을 깨닫는 데는 그리 긴 시간이 걸리지 않았다.
스케일만 한 함수 안으로 좁아졌을 뿐, 그 안에서 또 온갖 전역변수들을 덕지덕지 참고하여 결합도가 개판이 된 함수를 보는 듯한 느낌이어서 말이다. 소프트웨어공학 용어로는 common coupling에 해당한다.

여러 람다 함수가 비슷한 지역 변수들을 참조하는 경우, 그건 데이터와 함수를 몽땅 묶어서 함수 내부에 있는 local 클래스 형태로 떼어 내 버렸다. 어차피 람다의 캡처가 내부적으로는 그렇게 가상의 클래스 멤버 형태로 구현되니까 말이다.
그런데 클래스의 멤버 함수 내부에 있는 람다 함수가 지역 변수들도 참조하고 this 포인터까지 참조하는 경우.. 이건 좀 소속을 어디에다 둘지 좀 난감하긴 했다.

Visual C++ 2012부터는 드디어 캡처가 없는 람다에 한해 일반 함수 포인터로 typecast도 가능해졌다.

2. 템플릿 인자의 성격 분류

다음으로 C++ 템플릿 얘기를 하겠다.
지금까지 템플릿을 매크로 함수의 대체 용도로 사용했지만 한편으로는 이것을 공용체의 대체품으로도 잘 활용하고 있다.
예를 들어 클립보드의 내용을 type-safe하게 액세스 해 주는 클래스를 이런 식으로 사용한다.

CClipboard<WCHAR> cb(CF_UNICODETEXT);
CClipboard<DROPFILES> cb(CF_DROP);

예전에는 CClipboard라는 한 클래스 안에
union { PCWSTR pszText; DROPFILES *hDropFiles; };

라고 union을 쓰던 것을 typename T *pData 요렇게 바꾼 것이다.
어차피 한 클립보드 포맷 하에서 다양한 타입을 섞어서 쓸 일은 없으므로, 꼭 공용체가 필요한 상황이 아니기 때문이다.

그리고 템플릿 인자의 명칭을 용도별로 통일하고 있다. 크게 다음과 같은 네 가지가 있더라.

(1) 저것처럼 일반적인 임의의 type은 T이다.
(2) 참조 타입은 R이다. 이건 T 자체는 아니지만 call by value로도 전달할 수 있을 정도로 가볍고 T의 값을 온전히 나타낼 수 있는 key이다. T의 생성자에 단독 인자로 들어오는 게 가능하다.
가령, T가 String이라면 R은 const char*가 될 수 있다. 혹은 그냥 복사 생성자 격인 const T&도 상관없고.
R은 컨테이너 클래스다 추가하거나 검색하는 값을 지정할 때 쓰일 수 있다.

(3) 그리고 정수 인자는 N. 클래스에 소속된 static 배열의 원소 개수나 enum 값을 정의할 때 쓰인다.
(4) 끝으로, 람다든 functor든 함수 포인터든.. 어쨌든 실행 코드가 들어가는 부분은 O이다. F를 쓸까 하다가 기분 탓에 O가 됐다.

template<typename T, size_t N> class CStaticArray { T m_dat[N]; };
template<typename T, typename R=const T&> class CLinkedList { };
template<typename O> void EnumData(O func);

같은 식이다. 이것 말고 템플릿 인자의 용도는 또 뭐가 있을지 궁금하다.

다만, C++이 언어 문법 차원에서 템플릿 인자를 명백히 구분하는 건 정수형(N) 아니면 typename(T, R, O) 이렇게 둘뿐이다. 그러니 완전히 귀에 걸면 귀걸이이고 코에 걸면 코걸이이다. 템플릿 코드 자체만으로는 컴파일러가 문법 오류를 잡을 수 없으며, 그 템플릿에다 실제로 인자를 줘서 타입을 인스턴스화한 뒤에야 문제를 발견할 수 있다. 이것보다는 쓰임이 제한적이더라도, 좀 덜 와일드하고 통제 가능하고 쓰임을 예측 가능한 템플릿을 만들 수는 없나 싶은데 아직까지는 갈 길이 요원해 보인다.

아, 그나저나 템플릿 선언은 오로지 global scope 안에서만 가능하다. 한 함수 안에 있는 지역 클래스는 템플릿 형태로 선언될 수 없으며 일부 멤버 함수만이 템플릿 형태로 선언되는 것조차도 불가능하다.
람다 함수도 템플릿 형태로 선언될 수 없다. 미래의 C++ 문법에서는 이 정도 한계는 없어질 수도 있지 않겠나 하는 생각이 든다.

3. 전처리기에서 sizeof 연산자 떡밥

전처리기의 #if 조건식에서는 어지간한 정수 연산이 가능한 반면, 잘 알다시피 sizeof 연산자는 사용할 수 없다. sizeof의 값에 따라 곧장 조건부 컴파일이 가능하면 참 좋겠지만 그건 순리에 어긋나는 일이다. 컴파일러에게 넘겨 줄 코드를 전처리 하는 중인데, 임의의 구조체의 크기처럼 소스 코드를 컴파일해야만 값을 구할 수 있는 연산자를 전처리기에다가 요구할 수는 없기 때문이다.

과거에 C++에서 컴파일러와 링커 사이의 순리를 거스르는 시도를 하다 템플릿 export 흑역사가 발생했듯, sizeof도 전처리기와 컴파일러 사이에서 "닭이 먼저냐 계란이 먼저냐" 싸움이 될 수 있다.
전처리기를 이용한 조건부 컴파일러는 소스 코드의 이식성에 도움이 되는 기능인데, 정작 기계 종속적인 이식성 관련 정보를 무작정 표준 매크로 심벌로 집어넣을 수는 없다니 이거 참 역설이 아닐 수 없다.

물론 int나 void*의 크기 정도야 전처리기에다가 곧장 박아 넣을 수도 있다. 그러나 근본적으로 전처리기는 코드의 의미나 타겟 플랫폼 따위는 전혀 모른 채 시종일관 lexical한 치환만 하면서 완전 동일하게 동작하는 물건이며, 포인터 같은 기계 종속적인 사항에 대해서는 완전히 아오안이어야 한다.

이런 이유로 인해 포인터의 크기나 비트 endian-ness 같은 타겟 플랫폼 정보는 있으면 유용할 것 같음에도 불구하고 표준 predefined 매크로로 제공되지 않는다. 조건부 컴파일을 위해 그런 정보가 필요하다면 필요한 사람이 해당 매크로 심벌을 별도의 헤더나 컴파일러 옵션을 통해 지정해 놔야 한다. 전처리기가 월권 행위를 저지르지 않아도 되게 해 주기 위해서이다.

다만, 전처리기가 아닌 컴파일러의 입장에서는 sizeof는 컴파일 타임 때 값이 결정되는 아주 static하고 깔끔한 연산자이다. sizeof의 피연산자로는 타입 이름과 값이 모두 올 수 있는데, 값은 '직접 실행이 되지 않는다'. 쉽게 말해 sizeof( *((int *)NULL) )을 해도 뻑이 안 난다는 뜻.
그렇기 때문에 sizeof의 값은 static 배열의 크기를 결정하는 데 쓰일 수 있고, 또 case 문이나 템플릿의 정수값 인자에도 얼마든지 들어갈 수 있다.

Posted by 사무엘

2015/08/31 08:39 2015/08/31 08:39
,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1133

C++의 클래스에는 여느 객체지향 언어와 마찬가지로, 정적 바인딩이 아닌 실행 시간 동적 바인딩을 거쳐 호출되는 가상 함수라는 개념이 있다.
일반 함수들은 선언만 해 놓고 정의를 안 했다면, 그 함수를 실제로 호출한 곳이 있을 때만 링크가 시도되고 링크 에러가 난다. (물론 DLL을 만들 때처럼 외부로 export된다고 명시된 함수라면, 우리 모듈 내부에서 당장 안 쓰이더라도 당연히 몸체가 존재해야 함. 이건 논외로 한다.)

이런 일반 함수와는 달리 가상 함수는 해당 클래스에 속하는 개체가 생성되어 가상 함수 테이블이 만들어질 때 이미 참조가 발생한다. 그렇기 때문에 일단 선언을 한 이상, 코드 내부에서 직접적으로 호출을 안 하더라도 반드시 정의가 돼 있어야 한다. 그래야 링크 에러가 안 난다.
가상 함수이지만 자기 몸체가 존재하지 않아도 되는 예외적인 경우는 단 하나, 순수 가상 함수뿐이다.

순수 가상 함수가 존재하는 클래스는 반쯤은 불완전한 타입과 비슷하다. 불완전하기 때문에 그 클래스의 인스턴스를 직접 만들 수 없다. 누군가가 얘를 상속하여 파생 클래스를 만든 뒤, 모든 순수 가상 함수들에 대한 몸체를 구현해 줘야만 한다. 몸체를 안 만들고 선언만 해 놓으면 컴파일은 되지만 그제서야 링크 에러가 나게 된다. 즉, 순수 가상 함수는 가상 함수의 링크와 관련된 문제를 파생 클래스로 보류하는 역할을 한다.

순수 가상 함수가 들어있는 추상 클래스의 인스턴스를 직접 만드는 것은 컴파일러에 의해 원천 봉쇄되고 금지되어 있다. 그런데 아주 특수한 상황에서는 이렇게 몸체가 없는 추상 클래스에 대한 직접 호출이 일어날 때가 있다. 예를 들어 아래와 같은 경우이다.

class CDerived;
class CBase {
    CDerived *m_pt;
public:
    CBase(CDerived *p);
    virtual void Pure() = 0;
};

class CDerived: public CBase {
public:
    CDerived(): CBase(this) {}
    void Pure() { puts("Ahh"); }
};

CBase::CBase(CDerived *p): m_pt(p) { p->Pure(); }

int main() { CDerived obj; }

생성자에서 자기 자신이 아직 완전히 초기화되지 않은 상태일 때 Pure()을 호출해 버리니, 마치 열차가 탈선한 것처럼, 지뢰찾기에서 지뢰를 밟은 것처럼 허를 찔리는 것이다.
사실 forward 선언까지 동원해 가면서 굳이 m_pt를 CDerived의 포인터로 지정하지 않아도 된다. 그냥 기반 클래스와 동일한 CBase *로 지정해도 에러가 나는 건 마찬가지이다.

참고로, CBase의 생성자에서 저렇게 번거롭게 p->Pure()을 하지 않고 그냥 this에 대해서 Pure()을 호출하려 시도하면 그건 순수 가상 함수임에도 불구하고 동적 바인딩이 아니라 정적 바인딩이 일어난다. CBase에 Pure 함수가 정의돼 있지 않다고 링크 에러가 난다. 그걸 별도의 포인터 값으로 억지로 우회함으로써 아직 몸체가 없는 순수 가상 함수에 도달할 수 있다. 위의 코드는 굉장히 인위적인 예이지만, 멀티스레드 race condition 같은 데서도 드물게 이런 일이 벌어질 수 있다.

C#은 생성자에서도 동적 바인딩이 잘 처리되는 게 보장되는 반면, C++은 옛날에 만들어지기도 해서 성능 보전을 위해서인지 설계 관점이 다소 보수적이다. 기반 클래스의 생성자는 자신이 파생 클래스의 생성 과정의 일부라 하더라도 파생 클래스에 대한 단서를 전혀 얻을 수 없고 언제나 기반 클래스의 형태로만 동작한다.
여러 모로 생성자에서 가상 함수를 호출하는 건 좋지 않다는 걸 알 수 있다. DllMain 안에서 또 DLL을 로딩하지 말고 가능한 한 거기서는 몸을 사리고 있어야 하는 것과 비슷한 맥락이다.

초기화가 되기 전에 가상 함수 테이블이 가리키는 주소값이 NULL이었다면, 그냥 더 생각할 것도 없이 운영체제 차원에서 '잘못된 연산' 에러가 나고 그걸로 끝이 날 것이다. 그러나 그렇게만 하는 건 재미(?)가 없고 데이터를 조작하다가 발생한 여느 null pointer 에러와 구분도 어려우니 컴파일러는 가상 함수 테이블에다가 순수 가상 함수에 대한 디폴트 핸들러의 주소를 넣어 준다. 주소를 한 번만 넣어 주면 끝이니 딱히 성능에 영향을 받을 일이 없고, 따라서 debug뿐만 아니라 release 빌드에도 못 넣을 이유가 없다.

개체가 정상적으로 초기화됐다면 그 주소는 사용자가 작성한 파생 클래스의 함수로 당연히 대체된다. 그러나 그게 아직 바뀌지 않았다면 핸들러 함수로 간다. 얘는 assert(0)과 비슷한 급의 예외를 발생시키는 것 말고 딱히 하는 일은 없다. 다만, 이 일이 벌어졌을 때 사용자가 지정해 준 함수가 호출되게 할 수는 있다.

가상 함수가 몸체가 온전하지 않으면 컴파일 에러(순수 가상 함수에 대한 클래스 선언)와 링크 에러(...)뿐만 아니라 이렇게 런타임 에러까지 드물게나마 발생할 수 있다는 걸 알 수 있다. 런타임 에러라니 왠지 C++답지 않아 보이지만, 이건 그래도 운영체제나 컴퓨터 차원으로 갈 런타임 에러를 언어 시스템 차원에서 수습 가능한 런타임 에러로 바꿔 주는 체계까지는 갖춰져 있다. 배열 첨자 초과나 division by 0 에러는 전자로 바꾸려면 매번 값을 사전에 점검해야 하기 때문에 오버헤드가 큰 반면, 순수 가상 함수 호출은 그냥 디폴트 함수 주소만 설정해 주면 되니까 말이다.

Posted by 사무엘

2015/08/26 08:34 2015/08/26 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1131

네트워크나 파일 같은 외부 입출력을 열고 닫는 작업은 실패할 가능성이 워낙 높기 때문에 프로그램 작성에서 에러 처리가 거의 무조건적인 필수이다.

하지만 메모리 할당은 그렇지 않다. 수~수십 MB 정도 공간을 요청하거나 재할당하는 것쯤은 요즘 컴에서는 실패할 가능성이 0에 수렴한다. malloc의 결과값이 NULL인지 일일이 체크하는 코드는 요즘 거의 찾을 수 없다. C++의 new 연산자도 예전에는 실패 시 NULL 리턴이었지만 지금은 예외를 던지는 형태로 디자인이 바뀐 지 오래다. 그거 일일이 NULL 체크를 하는 건 너무 남사스럽고 성가시고 민망하기 때문이다.

메모리 할당을 위해 어지간해서는 C++의 깔끔한 new와 delete를 쓴다지만, C++ 연산자에는 메모리의 재할당 기능이 없기 때문에 이를 위해서는 여전히 malloc/realloc/free 쌍이 쓰인다. 그리고 좀 원시적인 테크닉이긴 하지만 가변 길이 구조체의 메모리 할당을 위해서도 크기 지정이 자유로운 C 스타일의 메모리 함수가 필요하다. 아니면 operator new 함수를 직접 호출하든가 말이다.

그런데 realloc은 실행이 실패했을 때의 상태가 꽤 복잡하다. 보통 ptr=realloc(ptr, newsize) 같은 형태로 활용을 하는데, 재할당이 실패했다고 생각해 보자. 이때는 realloc은 재할당을 할 수 없어서 NULL을 되돌린다. 이는 분명 비정상적인 오류 상황이고 프로그램이 그에 대한 별도의 대비를 하긴 해야 하지만, 그렇더라도 ptr이 원래 가리키던 메모리는 아무 이상이 없다. 그런데 ptr에다가 무턱대고 마치 malloc의 리턴값처럼 NULL을 대입해 버리면 ptr은 소실되고 메모리 leak이 발생하게 된다.

그러니 실행이 실패하더라도 메모리 leak은 발생하지 않게 하려면

ptr_tmp=realloc(ptr, newsize);
if(ptr_tmp) ptr=ptr_tmp; //성공
else { } //실패

번거롭지만 이렇게 임시 포인터 변수를 하나 추가로 둬서 실행이 성공했을 때에만 포인터의 실제값을 반영하게 해야 안전하다. 본인은 이 점을 한 번도 생각을 안 하고 있었는데 비주얼 C++ 2012에서부터 추가된 코드 정적 분석기가 지적을 해 주는 걸 보고서야 “아하!”하고 무릎을 쳤다.

이런 것을 생각하면 realloc의 실패야말로 리턴값보다는 예외 처리로 알려 주는 게 더 편리하겠다는 생각이 든다.
절차형으로 실행되는 컴퓨터 프로그램에서는, 당연한 말이지만 n+1단계 명령은 그 앞의 1~n단계의 모든 명령들이 성공적으로 차곡차곡 잘 실행됐다는 전제하에서만 실행 가능하다. 중간에 뭔가 탈이 났다면 더 진행을 할 수 없으며 어디까지 앞뒤로 되돌아가면 되는지를 컴퓨터가 스스로 판단할 수 없다. 컴퓨터에게는 인간 같은 유도리가 존재하지 않는다. 그렇기 때문에 그런 정보가 없다면 그 프로그램은 전체가 강제 종료되는 것밖에 답이 없다.

자동차 운전을 하는 사람이라면 단순히 핸들과 페달과 변속기를 조작하는 것 말고도 사고가 났을 때의 대처 요령과 보험사 연락처 같은 것도 숙지하고 있어야 하듯, 컴퓨터 프로그램도 마찬가지이다. 중간에 탈이 나도 최대한 부드럽게 수습하고, 피치 못할 상황에서  프로그램이 죽더라도 최소한 지금 작성 중인 문서를 저장이라도 한 뒤에 죽는 그 로직 자체도 프로그래밍이 돼 있어야 한다. 그것이 바로 예외 처리라는 분기 제어에 해당한다.
아울러, 숙달된 프로그래머라면 예외 처리를 구현하는 데 드는 추가 오버헤드와 비용을 숙지해 둘 필요도 있다. 수많은 객체들의 생명 주기를 관리하면서 여러 함수들을 한꺼번에 이탈하는 것도 그냥 될 리는 없으니 말이다.

C/C++은 애초에 운영체제/하드웨어 차원에서의 crash는 있어도 언어 차원에서의 예외 처리라는 게 아예 존재하지 않던 언어이다 보니 이쪽의 지원이 다른 언어들보다 상대적으로 미비하다. C++에 try/catch 키워드는 한참 뒤에 등장했으며 언어 자체는 이 예외 구문을 전혀 사용하지 않는다. 이걸 사용하는 건 라이브러리 계층에서이다. 그리고 예외 처리용 객체를 날려 줄 때조차도 new로 메모리를 할당했다면 해제를 수동으로 해 줘야 하니 불편한 점이 아닐 수 없다.

다시 본론인 realloc 얘기로 돌아온다.
저런 예외 처리도 오버헤드가 크니 싫고 리턴값만으로 모든 책임을 회피하고 싶다면, realloc 함수의 프로토타입을 차라리 이렇게 설계했으면 더 편했을지도 모른다.

bool realloc(void **pptr, size_t newsize);

void **라니 참 COM스러워 보이지만(CoCreateInstance, IUnknown::QueryInterface ㅋㅋ), C++이라면 템플릿 함수로 이걸 감싸서 지저분함을 한결 예방할 수 있을 것이다.

if(realloc((void **)&ptr, newsize)) { /* 성공 */ }
else { /* 실패 */ }
free(ptr);

내가 무엇을 의도하는지는 딱 보면 알 수 있을 것이다. 기존 메모리를 가리키고 있는 포인터의 주소를 받아서, 재할당이 성공하면 그 포인터가 가리키는 값을 그대로 바꿔 버리고 true를 되돌리는 것이다. 어차피 지금 realloc 함수는 ptr=realloc(ptr, newsize)라고 ptr이 함수 인자(input) 겸 리턴값(output) 형태로 동시에 쓰이고 있으며, 재할당이 성공했다면 예전 주소는 보관하고 있을 필요가 전혀 없으니 말이다.

실패했다면 ptr은 *ptr이든 **ptr이든 아무 변화가 없고 리턴값만 false가 된다. free(ptr)을 해 주는 한 어떤 경우든 메모리 leak 걱정은 안 해도 된다. realloc 함수가 이렇게 만들어지는 게 더 낫지 않았나 싶은 생각이 든다.
뭐, realloc이 결코 실패하지 않는다고 가정하고 프로그램이 막무가내로 동작한다면, 차라리 NULL 포인터 일대를 액세스하다가 확실하게 죽는 게 기존 메모리를 범위를 초과하여 건드리다가 죽는 것보다는 더 안전할지도 모르겠지만 말이다.

끝으로 하나 더. fopen에서 접근 모드와(read/write 등) 데이터 처리 모드(바이너리/텍스트) 인자는 들어올 수 있는 조합이 뻔하고 상수 명칭 조합으로 처리해도 하등 이상할 게 없을 텐데, 왜 하필 더 파싱도 어렵게 문자열을 쓰고 있는지도 이유를 모르겠다. 딱히 확장의 여지가 있어 보이지도 않는데 굳이 _open 같은 저수준 함수와 형태를 달리할 이유가 없다. 이런 것들이 C 라이브러리에 대해서 궁금한 점이다.

Posted by 사무엘

2015/04/14 08:25 2015/04/14 08:25
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1083

1. elseif 키워드

프로그래밍 언어에 따라서는 else if를 한데 묶은 축약형인 elseif 또는 elif 키워드를 별도로 제공하는 경우가 있다.
베이직이나 파이썬, 그리고 프로그래밍 요소 중에 없는 게 없는 백과사전형 언어인 Ada에는 저게 있다.

하지만 파스칼, C/C++이나 그 파생형 언어들은 전통적으로 그게 없다. 굳이 그걸 또 제공할 필요 없이 기존 if/else만으로도 동일한 표현력과 계산 능력 자체는 낼 수 있으며,
또한 더 큰 이유로는, 이들 언어는 안 그래도 공백이나 줄바꿈에 구애를 받지 않는 freeform 문법이기 때문이다. 필요하다면 어차피 else if를 한 줄에 나란히 연달아 써도 elseif와 얼추 비슷한 비주얼을 만들 수 있다. (컴파일러의 구문 분석 스택은 복잡해지겠지만..) 베이직과 파이썬은 그렇지 않다.

elseif 축약형은 else 절에서 실행되는 구문이 다음 if 절에 '완전히' 포함되어 있을 때 유용하다.
원래는 else 다음에 소스 코드의 들여쓰기가 한 단계 증가해야 하지만 그렇게 하기는 귀찮고..
수평적인 들여쓰기 단계에서 여러 개의 if를 대등한 위상에서 마치 switch-case처럼 늘어놓고 싶을 때 elseif가 쓰인다.

이런 점에서 보면 elseif 축약은 if-else에 대해서 tail-cut recursion을 한 것과 개념적으로 유사하다.
함수 재귀호출 뒤에 또 다른 추가적인 계산이 없다면, 그런 단순 재귀호출 정도는 스택을 사용하지 않는(= 한 단계 깊이 들어가는) 단순 반복문으로 바꾸는 것 말이다.

사실 C/C++은 elseif 축약이라는 개념은 언어 자체엔 없고 전처리기에만 #elif라는 형태로 있다.
전처리기는 알다시피 freeform 문법이 아니기 때문에 elif 없이 else와 if를 동시에 표현하려면 얄짤없이 줄 수가 둘로 늘어나야 하니,
문법을 최대한 간단하게 만들고 싶어서 부득이 그런 지시자를 넣은 것 같다.

2. NULL 포인터와 0

하루는 통상적으로 사용하던 #define NULL을 0에서 nullptr로 바꾸고 날개셋 코드를 리빌드해 봤다. 그랬더니.. 생각지 못했던 곳에서 엽기적인 컴파일 에러가 떴다.

아니 내가 머리에 총 맞았었나.. 왜 bool 변수에다가 NULL을 대입할 생각을 했지? =_=;;
HRESULT 리턴값에다가 S_OK 대신에 return NULL을 해 놓은 건 도대체 뭔 조화냐.
그리고 그 정도는 애교고.. obj=NULL이 원래는 컴파일 에러가 났어야 했는데 잘못된 코드를 생성하며 지나쳐 버리는 경우가 있었다. 포인터를 별도의 클래스로 리팩터링하는 과정에서 실수가 들어간 것이다.

그 클래스가 정수 하나를 인자로 받는 생성자가 있기 때문에 obj=Class(0)으로 자동으로 처리되고 넘어갔는데, 그 클래스는 독자적인 메모리 할당이 있으면서 대입 연산자 같은 것도 별도로 존재하지 않았다.
이런 일을 막으려고 C++엔 나중에 생성자에 explicit이라는 속성을 지정하는 키워드가 추가되었지만 그걸 사용하지 않는 레거시 코드를 어찌할 수는 없는 노릇이고..

아무튼 언어에서 type-safety를 강화하는 게 이렇게 중요하다는 걸 알 수 있었다.
Windows 플랫폼 헤더 include에서 NULL의 definition이 nullptr로 바뀌는 날이 언제쯤 올까? 옛날에 16비트에서 32비트로 넘어갈 때는 핸들 타입에 대한 type-safety를 강화하면서 STRICT 상수가 도입된 적이 있었는데.

NULL은 C 시절에 (void *)0, 초창기 C++에서는 타입 오버로딩 때문에 불가피하게 그냥 0이다가 이제는 nullptr로 가장 안전하게 변모했다.
개인적으론, PSTR ptr = false; 도 컴파일러 차원에서 안 되게 좀 막았으면 좋겠으나.. 포인터에 0상수 대입은 뭐 어찌할 수 없는가 보다.

3. 자바의 문자열

자바(Java)로 코딩을 하다 보면 나처럼 C++ 사고방식에 머리가 완전히 굳은 사람의 관점에서 봤을 때 궁금하거나 불편하다고 느껴지는 점이 종종 발견된다.
int 같은 기본 자료형이 아니면 나머지는 모조리 클래스이다 보니 한 함수에서 데이터 참조용으로나 잠깐 사용하고 마는 int - string 쌍 같은 것도 못 만드는지? 그런 것도 죄다 새 클래스로 만들어서 new로 할당해야 하는지?

그리고 기본 자료형은 값으로만 전달할 수 있으니 int의 swap 함수조차 만들 수 없는 건 너무 불편하지 않은지?
인클루드가 없는데 자신 외의 다른 클래스에 존재하는 public static final int값이 switch case 상수로 들어오는 게 가능한지? 등등..

이와 관련되어 문자열은 역시 자바 언어에서 좀 어정쩡한 위치를 차지하며 특이하게 취급되는 물건이다.
얘는 일단 태생은 기본 자료형이 아닌 객체/클래스에 더 가깝다. 그래서 타입의 이름도 소문자가 아닌 대문자 S로 시작하며, 이 개체는 가리키는 게 없는 null 상태가 존재할 수 있다.

그러나 얘는 문자열 상수의 대입을 위해서 매번 new를 해 줘야 하는 건 또 아니다. 이건 예외적으로 취급되는 듯하다.
그럼 그냥 String a; 라고만 하면 얘는 길이가 0인 빈 문자열인가(""), 아니면 null인가? 그리고 지역 변수일 때와 클래스 멤버 변수일 때에도 그 정책이 동일한가? 뭐 직접 회사에서 프로그램을 짜 본 경험으로는 전자인 것 같긴 하다.

단, 자바의 문자열을 다룰 때는 주의해야 할 점이 있다. 자바 프로그래머라면 이미 잘 숙지하고 계시겠지만, 문자열의 값 비교를 ==로 해서는 안 된다는 것이다. equals라는 메소드를 써야 한다.
==를 쓰면? C/C++식으로 얘기하자면 문자열이 들어있는 메모리 포인터끼리의 비교가 돼 버린다. 애초에 포인터의 사용을 기피하고 다른 걸로 대체하는 컨셉의 언어에서, 이런 동작은 99% 이상의 경우는 프로그래머가 의도하는 결과가 아닐 것이다.

C++에서야 문자열 클래스에 == 연산자가 오버로딩되지 않은 경우가 없을 테니 언어가 왜 저렇게 만들어졌는지 이해하기 어렵겠지만.. 자바는 연산자 오버로딩이란 게 없는 언어이며 String은 앞서 말했듯이 기본 자료형과 클래스 사이의 어중간한 위치를 차지하는 물건이기 때문에 이런 디자인의 차이가 발생한 듯하다. 자바는 안 그래도 걸핏하면 클래스 새로 만들고 get/set 등 다 메소드로 구문을 표현해야 하는 언어이니까.
오죽했으면 본인은 회사에서 자바 코드를 다루면서도 문자열 비교를 실수로 ==로 잘못 해서 발생한 버그를 발견하고 잡은 적도 있었다.

그나저나 유사 언어(?)인 스칼라, 자바스크립트 같은 언어들은 ==로 바로 문자열 비교가 가능했던 걸로 기억한다.

4. true iterator

파일을 열어서 거기에 있는 문자열을 한 줄씩 얻어 오는 함수(A), 그리고 각 문자열에 대해 출력을 하든 변형을 하든 일괄적인 다른 처리를 하는 함수(B)를 완전히 분리해서 별도로 작성했다고 치자. 혹은 한 디렉터리에 파일들을 서브디렉터리까지 빠짐없이 쭉 조회하는 함수(A)와, 그 찾은 파일에 대해서 삭제나 개명 같은 처리를 하는 함수(B) 구도로 생각할 수도 있다.
그런데 이 둘을 연계시켜서 같이 동작하게 하려면 어떻게 하는 게 좋을까?

이럴 때 흔히 떠올릴 수 있는 방법은,
A 함수에다가 B 함수까지 인자로 줘서 호출을 한 뒤, A의 내부 처리 loop에서 B에 넘겨줄 데이터가 준비될 때마다 B를 callback으로 호출하는 것이다. B는 간단한 일반 함수 + context 데이터 형태가 될 수도 있고, 아니면 가상 함수를 포함한 인터페이스 포인터가 될 수도 있다.

데이터 순회를 하는 A 자체도 파일을 열고 닫거나 내부적으로 재귀호출을 하는 등 state가 존재하기 때문에 매번 함수 실행을 시켰다가 종료하기가 곤란한 경우, 상식적으로 A를 먼저 실행시킨 뒤에 A가 계속 실행되고 있는 중(= 상태도 계속 유지되고)에 그 내부에서 B를 호출하는 게 바람직한 게 사실이다.
물론, 반복문 loop을 B에다가 두고, 반대로 B에서 A를 callback 형태로 호출하는 것도 불가능한 건 아니다. 그런데 프로그래밍 언어에 따라서는 이런 B 중심적인 사고방식의 구현을 위해 좀 더 획기적인 기능을 제공하는 물건도 있다.

def func():
    for i in [1,5,3]:
        yield i

a=func()
print a.next()
print a.next()
print a.next() # 예상하셨겠지만 1, 5, 3 순서대로 출력

파이썬에는 함수에 return 말고 yield 문이 있다. 그러면 얘는 함수 실행이 중단되고 리턴값이 지정되기는 하는데..
다음에 그 함수를 실행하면(정확히는 next() 메소드 호출 때) 처음부터 다시 실행되는 게 아니라, 예전에 마지막으로 yield를 했던 곳 다음부터 계속 실행된다. 예전의 그 함수 호출 상태가 보존되어 있다는 뜻이다.

난 이걸 처음 보고서 옛날에 GWBASIC에 있던 READ, DATA, RESTORE 문과 비슷한 건가 싶었는데.. 저건 당연히 GWBASIC을 아득히 초월하는 고차원적인 기능이다. C++이었다면 별도의 클래스에다가 1, 5, 3 static 배열, 그리고 현재 어디까지 순회했는지를 가리키는 상태 인덱스 정도를 일일이 구현해야 했을 텐데 저 iterator는 그런 수고를 덜어 준다.

단순히 배열이 아니라 binary tree의 원소들을 prefix, infix, postfix 방식으로 순회한다고 생각해 보자.
순회하는 함수 내부에서 다른 콜백 함수를 호출하는 게 아니라 매번 원소를 발견할 때마다 리턴값을 되돌리는 형태라면..
구현하기가 굉장히 까다로울 것이다. 스택 메모리를 별도로 할당한 뒤에 재귀호출을 비재귀 형태로 일일이 구현해 주거나, 아니면 각 노드에다가 부모 노드의 포인터를 일일이 갖춰 줘야 할 것이다.

C++의 map 자료형도 내부적으로는 RB-tree 같은 자가균형 dynamic set 자료구조를 사용하는데, 이런 iterator의 구현을 위해서 편의상 각 노드에 부모 노드 포인터를 갖고 있는 걸로 본인은 알고 있다. RB-tree는 내부적으로 로직이 굉장히 복잡하고 까다로운 자료구조이긴 하지만, 그래도 부모 노드 없이도 구현이 불가능한 건 아닌데 말이다.
안 그랬으면 iterator가 자체적으로 스택을 멤버 변수로 갖거나, 최소한 메모리 할당· 해제를 위해 생성자나 소멸자까지 갖춰야 하는 복잡한 class가 돼야 했을 것이다. 어떤 경우든 포인터 하나와 비슷한 급인 lightweight 핸들이 될 수는 없다.

개인적으로는 지난 여름에 <날개셋> 한글 입력기 7.5에 들어가는 새로운 한글 입력 순서 재연 알고리즘을 구현할 때 비슷한 레벨의 iterator를 비재귀적으로 구현한 적이 있는지라, yield문의 의미가 더욱 절실히 와 닿는다.

Posted by 사무엘

2015/02/25 08:38 2015/02/25 08:38
, , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1066

1. 오픈소스

잘 알다시피 C/C++은 메모리 할당이나 문자열 등, 바이너리 차원에서 뭔가 언어나 구현체가 buliding block을 규정해 놓은 게 없다시피하며, 그나마 표준이 나온 것도 강력한 구속력을 갖고 있지는 못하다. 그러니 이 지저분함을 참다 못해서 COM 같은 바이너리 규격이 나오고 닷넷 같은 완전히 새로운 프레임워크도 나왔다.

아니면 일각에서는 소프트웨어 컴포넌트를 재배포할 때, 빌드된 라이브리러리를 주는 게 아니라 난독화 처리만 한 뒤 소스 코드를 통째로 넘겨주면서 빌드는 이 코드를 쓰는 쪽에서 자기 입맛대로 알아서 하라는 극단적인 조치를 취하기도 한다. 차라리 오픈소스 진영이 이런 점에서는 융통성이 더 있는 셈이다.
하지만 어지간한 컴덕력을 갖추지 못한 사람은.. 복잡한 빌드 시스템/configuration들을 이해할 수 없어서 소스 코드까지 통째로 줬는데도 줘도 못 먹는 촌극이 벌어진다.

이런 라이브러리 내지 유닛, 패키지는 기계어 코드로든 다른 바이트 코드로든 소스 코드가 바이너리 형태로 용이하게 재사용 가능한 형태로 가공되어 있는 파일이다.
그런데 실행문이 들어있는 소스 코드가 반드시 그대로 노출돼야만 하는 분야도 있다.

크게 두 갈래인데, 하나는 C++의 템플릿 라이브러리이고, 다른 하나는 웹 프로그래밍 언어 중에서도 전적으로 클라이언트 사이드에서 돌아가는 자바스크립트이다.
동작하는 환경 내지 타겟은 둘이 서로 완전히 극과 극으로 다르지만, 전자는 컴파일 때 최적화 스케일의 유연성 때문에, 그리고 후자는 선천적으로 기계 독립적이고 극도로 유연해야만 하는 웹의 특성상 오픈소스가 강제되어 있다.

자바스크립트는 비록 전통적인 기계어 EXE를 만드는 데 쓰이는 언어는 아니지만 그렇다고 해서 만만하게 볼 물건이 절대로 아니다. 람다, 클로저 등 어지간한 최신 프로그래밍 언어에 있는 기능은 다 있으며, 플래시에 하드웨어 가속 3D 그래픽까지 다 지원 가능한 경지에 도달한 지가 오래다.
또한 웹에서의 영향력이 워낙 막강하다 보니 전세계의 소프트웨어 업체들이 눈에 불을 켜고 실행 성능을 필사적으로 끌어올려 놓았다. 비록 컴파일을 통한 보안 유지는 안 되지만, 어느 수준 이상의 코드 난독화 기능도 당연히 있다.

뭐, C++ 표준 템플릿 라이브러리도 헤더 파일을 열어 보면, 남이 못 알아보게 하려고 코드를 일부러 저렇게 짰나 싶은 생각이 든다. 온갖 주석이 곁들여져서 알아보기 쉽게 널널하게 작성된 C 라이브러리의 소스들과는 형태가 달라도 너무 다르다..

C++ 템플릿에 대해서 한 마디 더 첨언하자면.. 제한적으로나마 함수나 몸체를 일일이 인클루드해서 노출하지 않아도 되는 방법이 있긴 하다.
몸체를 한 cpp(= 번역 단위)에다가만 구현해 놓은 뒤, 거기에다가 소스 코드 전체를 통틀어 그 템플릿이 인자가 주어져서 쓰이는 모든 형태를 명시만 해 주면 된다.

template Sometype<char>;
template Sometype<wchar_t>;

템플릿 함수에 대해서 template<> 이렇게 시작하는 특정 타입 전용 케이스를 만드는 것과 비슷해 보이는데..
위와 같은 식으로 써 주면, 해당 코드가 컴파일될 때 이 템플릿이 저런 인자로 실현되었을 때의 대응 코드가 모두 생성되고, 이게 다른 오브젝트 파일들이 링크될 때 같이 연결되게 된다. 이런 문법이 있다는 것을 15년 동안 C++ 프로그래밍을 하면서 처음 알았다.

물론 저것 말고 다른 임의의 새로운 타입으로 템플릿을 사용하고 싶다면 그렇게 템플릿을 사용하는 번역 단위에서 또 다시 템플릿의 선언부와 몸체를 싹 읽어들여서 분석을 해야 한다.
아마 과거의 export 키워드가.. 저런 템플릿 인자의 사용 형태를 자동으로 파악하는 걸 의도하지 않았나 싶은데 그래도 세상에 쉬운 일이란 없었던 듯하다.

2. 웹 프로그래밍의 성격

HTML, CSS, 자바스크립트 삼신기는 마치 웹 프로그래밍계에서의 삼권 분립이기라도 한 것 같다. 아무래도 당장 화면에 표시되는 핵심 컨텐츠가 HTML이니 요게 행정부에 대응하는 듯하며, HTML을 표시할 규격을 정하는 CSS는 사법부에 가깝다. 끝으로, 인터랙티브한 동작을 결정하는 자바스크립트는 입법부 정도?
물론 HTM 파일 하나에다가 스타일과 자바스크립트 코드를 다 우겨 넣었다면 그건 뭐 “짐이 곧 국가다, 법이다” 식으로 코드를 작성한 형태일 것이다.

예로부터 본인이 느끼기에 웹 프로그래밍은 뭔가 시대의 최첨단을 달리는 것 같고 간지와 뽀대가 나고 실행 결과가 사용자에게 가장 직접적으로 드러나 보이는 신기한 영역인 것 같았다. 하지만 (1) 코드와 데이터, 클라이언트와 서버, 코딩과 디자인의 역할 구분이 영 모호하며, 컴퓨터의 성능을 100% 뽑아내는 듯한 전문적이고 하드코어한 느낌이 안 들어서 마음에 안 들었다. 가령, 도대체 어디서는 java이고 어디서는 jsp이고 어디서는 js인지?

(2) 또한 이 바닥은 작성한 소스 코드가 제대로 보호되지 못한다. 서버 사이드에서만 돌아가는 PHP 같은 건 클라이언트에게는 노출이 안 되겠지만 그것도 서버 개발자들끼리는 결국 오픈소스 형태로 공유될 수밖에 없으니 말이다. 옛날에 제로보드의 소스가 그랬듯이.

끝으로, (3) 특정 CPU 아키텍처나 플랫폼에 구애되는 게 없다 보니 기반이 너무 붕 뜨는 느낌이고, 브라우저마다 기능이 제각각으로 달라지는 거 호환 맞추는 노가다가 필요한 것도 싫었다.
뭐, IE와 넷스케이프가 경쟁하고 IE6이 세계를 사실상 평정했던 먼 옛날에는 그랬고 지금은 이 문제는 많이 해소됐다. 바야흐로 2015년, HTML5 표준안까지 다 완성된 지경이니, 웹 프로그래밍도 이제 충분히 성숙했고 기반이 탄탄히 잡혔다. 격세지감이다. ActiveX도 점점 퇴출되는 중이다.

2004년에 IE6에 대한 대항마로 파이어폭스 0.8이 혜성처럼 등장했고, 2008년엔 구글 크롬이 속도 하나로 세계를 평정해서 IE의 독점 체계를 완전히 견제해 냈다. 지금은 크롬이 속도는 괜찮은 반면, 메모리 사용량이 너무할 정도로 많아서 파이어폭스가 다시 반사 이득을 보는 구도이다. 오페라는 Windows에서는 영 좀 마이너한 콩라인 브라우저가 아닌가 모르겠다.
그리고 무슨 브라우저든지 버전업 숫자 증가폭이 굉장히 커졌으며, 탭 브라우징에  메뉴와 제목 표시줄을 숨겨 놓는 인터페이스가 필수 유행이 돼 있다.

3. 보안 문제

세월이 흐르면서 웹 프로그래밍 환경이 좋아지고 있는 건 사실이지만, 보안 때문에 예전엔 바로 할 수 있었던 일을 지금은 못 하고 뭘 허가를 얻고 번거로운 절차를 거쳐야 하는 건 다소 불편한 점이다.
특히 내가 느끼는 게 뭐냐 하면, 한 HTML 파일에서 자신과 다른 도메인에 있는 CSS나 JS 같은 걸 덥석 인클루드 하는 걸 브라우저가 굉장히 싫어하게 됐다는 점이다. 이런 걸 이용한 보안 취약점 공격이 지금까지 많았는가 보다.

"이 사이트에는 안전한 컨텐츠와 위험한 컨텐츠가 같이 섞여 있습니다. 위험한 것도 모두 표시하시겠습니까?"라는 메시지가 바로 이런 상황에서 뜬다.
IE의 경우 예전에 잘 표시되던 사이트가 갑자기 표시되지 않을 때, 권한 취득을 위해 레지스트리에다 자기 프로그램 이름이나 사이트를 등록하는 등 조치를 취해야 했다.
구글 크롬은 발생 조건이 IE와 동일하지는 않지만, 자체 판단하기에 악성 코드의 실행을 유도하는 걸로 의심되는 지시문이 HTML 소스에 있는 경우, 화면 전체가 위험 경고 질문 화면으로 바뀐다.

최근에는 크롬과 IE에서는 멀쩡하게 보이는 웹 페이지가 파이어폭스에서만 제대로 표시되지 않는 문제가 있어서 회사 업무 차원에서 사이트 디버깅을 한 적이 있었다. 요즘 세상이 무슨 세상인데 웹 표준이나 렌더링 엔진의 버그 때문일 리는 없고, 파이어폭스가 자바스크립트 엔진으로 하여금 외부 도메인로부터 인클루드된 CSS 속성에 접근하는 걸 허용하지 않아서 발생한 문제였다.

4. 파일 관리가 되는 게시판

본인도 여느 프로그래머와 마찬가지로 다니는 회사에서 요즘 모바일에 웹까지 별별 걸 다 손대며 지냈다. 하긴, 공학 박사라 해도 취업 후에는 돈 되는 분야, 뜨는 분야를 따라 자기 주전공 연구 분야가 아닌 것도 손대 봐야 할 텐데 하물며 그보다 급이 낮은 단순 엔지니어들은 말이 필요하지 않을 것이다.

요즘은 게시판이나 블로그 엔진을 만들려면 단순무식한 텍스트 기본 폼이 아니라 위지윅 웹 에디터가 필수이다. ckeditor 컴포넌트에다가 이미지 업로드 기능을 연결해 넣을 일이 있었는데 이것도 여간 골치아픈 일이 아니라는 걸 작업을 하면 할수록 깨닫게 됐다.
손이 정말 많이 간다. 하지만 그걸 일일이 하지 않으면 이미지는 단순 외부 링크밖에 못 넣는 반쪽짜리가 된다.

이미지 파일이 하나 HTTP 규격대로 업로드되어 왔으면 서버 측에서는(PHP든 JSP든 무엇이든) 파일 크기가 적당한지(개별 파일 크기와 지금까지 업로드된 파일의 전체 크기 모두) 체크하여 적당하다면 이름을 중복 없는 랜덤 이름으로 바꿔서 서버에 저장한다. 이름에 한글이 들어간 파일이라고 업로드나 로딩이 제대로 안 되는 일이 없어야 하니까.

그 뒤에 그 그림을 불러올 수 있는 URL을 에디터 컴포넌트에다가 알려 준다. 이것도 간단하게 만들자면 그냥 서버의 특정 디렉터리를 그대로 노출하는 식으로 만들면 되겠지만 보안상 위험하니 가능한 한 제3의 장소에서 파일을 되돌리는 서버 프로그램 URL을 주는 게 안전하다.

위지윅 에디터에서는 임의의 개수의 파일이 업로드될 수 있기 때문에 글에 얽힌 첨부 파일들을 따로 디렉터리나 DB 형태로 관리해서 글이 삭제될 때 같이 지워지게 해야 한다.
사실, 이쪽으로 조금만 더 신경 쓰면 글별로 아예 첨부 파일 관리자라도 간단한 형태로 만들어야 하게 된다. 우와..;;

그리고 골때리는 건, 아직 작성 중이고 정식으로 등록하기 전의 임시 상태인 글에 첨부된 그림들을 처리하는 방식이다.
일단은 그림들이 임시 폴더에다가 올라가고 주소도 임시 폴더 기준이지만 글이 정식으로 등록됐다면 글 중에 삽입된 이미지들의 주소를 수동으로 바꿔야 하고 파일도 옮겨야 한다.
또한 그 상태로 글이 더 등록되지 않고 사용자가 back을 눌렀다면, 서버에 올라왔던 임시 파일들도 나중에 지워 줘야 한다. 이런 것까지 도대체 어떻게 다 구현하지?

이건 일게 위지윅 에디터 컴포넌트가 감당할 수 있는 수준이 아니기 때문에 그걸 블로그 엔진이나 게시판에다 붙여 쓰는 웹 프로그래머가 자기 서버의 사정에 맞게 세팅을 해야 한다.
겨우 이미지 업로드 기능 하나만 달랑 구현하는 테크닉을 소개한 블로그만으로는 정보가 너무 부족했다.
Windows에서 공용 컨트롤에다 드래그 드롭을 처음부터 직접 구현하는 것만큼이나 손이 많이 갔다. 나 같은 이 바닥 초짜로서는 그저 경악스러울 뿐.

프로그램의 완성도를 더 높이려면, 사용자가 곱게 이미지 파일만 올리는 게 아니라 php나 html 같은 보안상 위험한 파일을 올리는 건 아닌지 감시해야 한다. 첨부 파일 정도가 아니라 위지윅 웹 에디터 자체도 위험하다고 그런다. HTML이 근본적으로 문서와 코드가 뒤섞인 형태이다 보니 정말 매크로가 잔뜩 든 Office 문서처럼 취급되는가 보다.
아무튼, 나모 웹에디터와 제로보드가 뜨던 시절에 비해 요즘 웹은 너무 방대하고 복잡하다.

Posted by 사무엘

2015/02/02 08:39 2015/02/02 08:39
, , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1057

요런 개념을 표로 일목요연하게 한 번쯤 정리할 필요를 예전부터 느꼈던지라, 잠시 짬을 내어 만들었다. C++ 프로그래머라면 고개가 절로 끄덕여질 내용 되겠다. 

토큰 type 앞 type 뒤 value 앞 양 value 사이 value 뒤
&   참조자형 명시 address-of (L-value만) 비트 AND  
&&   R-value 참조자형 명시   논리 AND  
*   포인터형 명시 배열 및 포인터 역참조 곱셈  
±     양/음 부호 덧셈/뺄셈  
괄호() (1) 형변환(typecast)
(2) 타입 선언자 나열 순서 조절
함수형 명시 연산 순서 조절   함수 호출
대괄호[]   배열형 명시     배열 참조
부등호<>   템플릿 인자 명시   비교 템플릿 함수 인자
콤마,       (1) 콤마 연산
(2) 함수 호출/템플릿 인자 구분
(3) 변수 선언 구분
 

  • 괄호는 영어에서 접속사도 되고 지시형용사/지시대명사, 관계대명사까지 다 되는 that만큼이나 정말 다재다능한 물건이다.
  • 베이직은 이례적으로 =, ()가 쓰임이 중첩되어 있다. =가 대입과 동등 연산을 모두 담당하며, ()가 함수 호출과 배열 첨자를 모두 담당한다. 함수 호출은 문법적으로 매우 제한된 문맥에서만 허용되니, C/C++같은 함수 포인터가 존재하지 않는다면 () 중첩이 아주 불가능하지는 않은 듯하다.
  • C/C++은 @ $ ` 기호를 전혀 사용하지 않는다. 예전에 베이직은 각종 기괴한 기호들을 이용하여 변수의 자료형을 표현하곤 했다. A$는 문자열, A%는 정수, A#은 실수 같은 식이다.
  • 파스칼은 포인터형을 선언하는 토큰이 ^인데, C/C++와는 달리, 포인터형을 나타낼 때와 포인터를 역참조할 때 토큰이 등장하는 위치가 서로 다르다. ^Type 그리고 Value^ 이런 식.
  • int *a와 int a[5]배열에 대해서는 똑같이 *a를 쓸 수 있지만, 잘 알다시피 배열의 역참조와 포인터의 역참조는 개념적으로 다르다. C/C++을 처음 공부하는 초보자가 굉장히 혼동할 수도 있을 듯하다.
  • 포인터는 다중 포인터가 존재할 수 있고 역참조도 여러 단계를 연달아 할 수 있다. 그러니 *가 여러 개 연달아 올 수 있다. 그 반면, 참조자는 구조적으로 딱 한 번만 참조/역참조가 가능하게 만들어진 포인터의 축소판이다. 그렇기 때문에 &&에다가 별도로 R-value 참조자라는 물건을 집어넣을 수도 있다. 이걸 생각해 낸 고안자는 정말 천재다.
  • 일반적으로 &는 address-of 연산자이며 R-value를 상대로는 적용이 되지 않는다.
    그러나 일부 값은 L-value가 아님에도 불구하고 & 연산자의 적용이 가능하며, 심지어 a와 &a가 동일하게 취급되는 것도 있는데, 바로 static 배열과 일반 함수이다.
    기본적으로 포인터의 성격을 갖추고 있는지라 &를 안 해도 기본적으로 자신의 주소가 되돌아오고, &를 붙여도 무방하다는 오묘한 특징이 있다.
  • 한편, C/C++에서 배열은 고유한 자료형임에도 불구하고 함수의 인자로 전달되거나 리턴값이 될 때는 그냥 포인터든 배열의 포인터든 포인터의 형태로만 전달된다. 배열 그 자체가 전달되지는 못한다.
    배열을 생으로 함수를 통해 주고 받으려면 구조체에다가 배열을 집어넣어야 한다.

Posted by 사무엘

2015/01/02 19:39 2015/01/02 19:39
,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1046

PL(프로그래밍 언어)계에서 함수형 프로그래밍 언어는
자동차 엔진으로 치면 로터리 엔진, 발전소 업계로 치면 핵융합 발전 같은 뭔가 이상은 높지만 현실은 아직 좀 시궁창인 그런 떡밥스러운 영역으로 간주되는 것 같다.

전산학을 전공해서 PL 수업을 들은 분이라면 이미 잘 아시겠지만, 프로그래밍 언어란 크게 절차형과 선언형으로 나눌 수 있다.
절차형은 튜링 기계라는 컴퓨터의 특성을 그대로 반영하여 메모리로부터 값을 읽은 뒤 연산을 수행해서 값을 변경하고, 메모리 위치도 바꾸는 절차를 순차적으로 일일이.. 즉 HOW 위주로 기술하는 언어이다. 정보 올림피아드 경시부가 다루는 것은 응당 절차형 프로그래밍 언어를 활용하여 프로그램을 작성해서 문제를 해결하는 능력이다.

(덧붙이자면 튜링 기계에다가 데이터뿐만 아니라 코드, 즉 상태 변환 로직까지 동일한 메모리에다 올려서 해석하는 계산 모델이 바로 오늘날 컴퓨터의 근간이 된 프로그램 내장형, 즉 폰 노이만 모델이다. 자동차 엔진로 치면 정말 외연 기관에서 흡입-압축-폭발-배기 4행정 내연 기관으로 변모한 수준의 발전이 아닌가 싶다.)

한편, 선언형은 우리가 원하는 솔루션의 정의 내지 조건이 이러하다.. 라고만 써 주는 형태의 WHAT 지향형 언어이다. 그러면 컴퓨터가 알아서 문제를 풀어 낸다.
따지고 보면 데이터베이스 질의어인 SQL은 DLL, EXE 같은 실행 파일을 만드는 용도의 언어가 아닐 뿐이지 아주 대표적인 선언형 언어이다. 복잡한 DB에서는 질의어를 만드는 것도 굉장히 복잡한 일이 되며, 두 DB간의 JOIN은 어떻게 시키고 어느 구문부터 풀이해서 최적의 성능으로 질의를 수행할지 결정하는 것도 아주 어려운 축에 든다. 이런 거 성능 소요 시간을 몇 % 단축시키는 알고리즘을 개발해 내면, DB를 연구하는 전산학과 대학원 연구실에서는 그게 곧바로 논문감이 된다.

흔히 인공지능 문제 풀이형 언어로 알려져 있는 프롤로그도 선언형 언어이다. 이건 진짜 여러 변수들을 선언한 뒤 변수들간의 인과관계를 쭈욱 나열해 주면 이를 바탕으로 언어 런타임이 문제의 답을 찾아 낸다.

까놓고 말해 절차형 프로그래밍 언어로 "아인슈타인의 퍼즐" 같은 걸 풀려면, 프로그래머가 재귀호출에 각종 백트래킹 알고리즘을 직접 구사해야 하니 앞에서 말했던 정보 올림피아드 경시부 급의 기술이 필요하다. 그러나 프롤로그에서는 "영국인.집 = 빨강, 스웨덴.애완동물 = 개" 이런 식으로 단서만 주어진 규칙대로 쓴 뒤 쿼리를 날리면 금붕어를 기르는 사람의 국적을 구할 수 있다.

아마 네모네모 로직이나 스도쿠 같은 것도 해답이 갖춰야 할 조건을 명시하는 것만으로 바로 풀 수 있지 싶다. 단서들을 바탕으로 뺑뺑이를 돌리는 추론 과정은 언어 런타임 내지 엔진이 해 준다.
대학 학부 시절, OR개론 수업 때 잠시 접했던 선형계획법 문제 풀이 프로그램인 k-opt도 역시 지정된 문법에 따라 변수와 부등식들을 써 놓고 최소화/최대화 조건을 명시하면 프로그램이 해를 찾아 주니.. 일종의 선언형 프로그래밍 언어 런타임에 속한다고 할 수 있겠다.

그러니, 절차형 언어의 컴파일러는 최적화를 하는 게 기계어 코드 생성이나 멀티코어 병렬화 같은 아주 미시적인 것과 관계가 있는 반면, 선언형 언어의 수행 방식을 최적화하는 것은.. 거시적인 알고리즘 전략까지 결정해야 하니 더욱 까다로울 것이다. 미시적인 건 해당 언어 엔진이 아주 정교하게 구현되어 있지 않은 이상 신경 쓰기 힘들다.

앞서 언급한 SQL이나 프롤로그는 선언형 프로그래밍 언어 중에서 일종의 '논리 지향'인 물건들이다. 그런데 선언형의 하위 범주로는 '함수 지향', 함수형 프로그래밍 언어라는 게 있다. 이게 절차형보다는 좀 더 수학자스러운 형태로 컴퓨터를 부려먹는 방법을 기술하는 방법론이라고 한다. (함수형이 여느 절차형 프로그래밍과 계산 능력이 동등하다는 것은 튜링 기계와 람다 대수가 동치라는 것이 증명됨으로써 알려져 있다.)

순수한 함수형 프로그래밍 언어에서는 지저분한 대입 연산이 없고 한번 생성된 값은 변경 없이 계속 그 값으로 남아 있다. 새로운 값이 계속 생성될 뿐이다. 사실 문자열을 이런 사고방식으로 처리하는 라이브러리나 언어, 프레임워크에서는 이미 있는 문자열을 변경하는 게 굉장히 어렵기도 하다. Windows RT의 String 클래스도 그랬던 걸로 기억..

함수형 언어에서는 대입이 없으니 응당 뺑뺑이 loop도 있을 수 없다. loop을 대신하는 것은 재귀호출이다! loop조차도 기존 값을 계속 바꾸는 게 아니라 새로운 값을 자꾸 만들어 내는 방식으로 구현된다는 뜻이다.
처음에 해답의 범위가 0부터 100 사이에 있었다면 그 다음 턴에는 0부터 50 (log n 시간 복잡도), 혹은 0부터 99로 자가호출이 이뤄지고, 이것이 문제가 완전히 해결될 때까지 반복된다. 왜냐하면 이 문제의 솔루션이 바로 그런 형태로 귀납적으로 정의돼 있기 때문이다. 팩토리얼이든, 두 수의 최대공약수이든, 정렬이든 다른 무엇이든.

이 패러다임에서는 함수가 다른 여느 데이터와 완전히 동일한 수준으로 다른 함수의 인자가 될 수 있고, 특히 이름 없이 함수의 몸체만을 여느 값처럼 달랑 전해 줄 수 있고, 다른 함수로부터 합성되고 유도된 새로운 함수가 함수의 리턴값이 될 수 있다. 새로운 함수가 동작하는 데 필요한 주변 문맥은 클로저라는 물건이 알아서 다 처리해 준다.
C/C++의 함수 포인터에 머리가 굳은 프로그래머라면 calling convension은 무엇인지, this 포인터가 포함된 멤버 함수인지, pointer-to-member라면 다중 상속으로 인한 부가 오버헤드는 없는지 같은 디테일 때문에 머리가 복잡해질 것이다.

함수형 언어에서 if문은 응당 자기 자신도 조건이 만족하는 쪽의 값을 되돌리는 함수 형태이다.
그러나 if는 조건이 만족하는 쪽만 '계산'이 행해질 터이니 if(a) b; else c; 를 나타내는 if(a, b, c)는 통상적인 함수 호출 func(a, b, c)와 의미상으로 완전히 동일할 수는 없다. 예약어로 따로 해석되고 취급을 받아야 할 듯하다.

물론 이런 함수형 프로그래밍 언어가 구현되기 위해서는 현실에서 컴파일러가 최적화해 줘야 하는 것, 그리고 언어 런타임이 해 줘야 하는 오버헤드가 적지 않다. 끝없이 새로운 값을 생성해 내더라도 이제 참조가 끝나서 더 쓰이지 않는 값은 GC가 알아서 제거해 줘야 하고, 재귀호출, 특히 tail recursion 정도는 알아서 메모리 복잡도를 O(n) 급으로 늘리지 않는 일반적인 loop처럼 돌아가게 컴파일러나 런타임이 최적화를 해 줘야 한다. 함수를 값처럼 부드럽게 다루는 것도 기술적으로는 단순 함수 포인터 이상의 추상화 계층이 필요하며, 말처럼 쉬운 일이 아니다.

예를 들어.. X라는 함수가 있는데.. 얘는 a라는 인자를 받고는,
b라는 인자를 받아서 a에다가 b를 더한 값을 되돌리는 Y라는 함수를 되돌린다고 치자.
결국 Y는 X라는 함수가 호출될 때 전달되었던 매개변수 내지 그때 생성된 X 내부의 지역 변수에 의존하여 동작하는데..
나중에 Y가 단독으로 호출될 때는 X라는 함수는 실행이 끝나고 그 문맥이 존재하지 않는다. 이를 어찌하리?
이런 딜레마를 피하기 위해 C/C++ 언어는 애초에 함수 안에 함수를 만드는 걸 지원하지 않는 쪽으로 설계되었으며, C++의 functor 같은 것도 전부 자기가 자체적으로 데이터 멤버를 갖고 있는 객체 형태로 만들어지게 된 것이다.

또한, 아무리 대입이 사이드 이펙트가 남는 지저분하고 기피되어야 하는 연산이고.. 다른 모든 연산을 loop 대신 재귀호출로 때운다 해도.. 당장 외부 파일/키보드로부터의 input은.. 대입 연산 없이는 감당이 도저히 불가능하다. 그리고
return t1.len() > t2.len() ? t1: t2
처럼 그 재귀호출의 결과값을 취사 선택하는 간단한 판단을 위해서라도 임시 변수에 대입하는 것 정도는 근본적으로 전혀 없을 수가 없다.
어디 그 뿐이랴. 대용량의 단일 데이터를 대상으로 여러 함수들이 포인터만 주고받으며 동작하다 보면, 한 함수가 자기 argument 안에 입출력 대상인 모든 데이터를 집어넣는 것은 무리가 있다.

허나 함수형 프로그래밍이 성능면에서 불리한 요소만 있는 건 아니다. 대입으로 인한 side effect 같은 게 없으니 소스 코드의 정적 분석은 더 용이할 것이고 병렬화 등 입맛에 맞는 최적화에도 더 유리할 것이다. 애초에 선언형 프로그래밍 언어는 구체적인 실행 방식은 그런 똑똑한 컴파일러나 언어 엔진에게 맡기고 있으니까.
이러니 PL 분야를 연구하는 전산학자나 수학 덕후들이 함수형 프로그래밍 언어에 더욱 열광하는 듯하다. 저런 패러다임이 응집도· 결합도 같은 소프트웨어 공학적인 측면에서 더 깔끔한 코드를 만드는 데 도움이 되는 것은 덤이다.

대학교 전산학과에서는 함수형 프로그래밍 언어로 보통 Scheme을 실습하는 편이다. 본인도 먼 옛날 학부 시절에 '프로그래밍의 이해(PP)'라는 과목을 통해 그 물건을 접했으며, 그걸로 무슨 다항식의 곱셈을 하는 프로그램도 숙제로 만들고 여러 덕질을 했었다. 함수형 언어의 진짜 본좌라고 일컬어지는 Haskell 같은 건 난 모름.;;

여담이지만 지금 생각해 보니, 온갖 복잡한 괄호가 배배 꼬여 있는 Scheme 코드는.. 언어학에서 문장 구문 분석을 괄호로 표현해 놓은 것과 사뭇 닮았다는 생각이 들었다. (S (NP .. ) (VP ...)) 이러는 식.
Schme에서는 S 대신에 define, lambda, if 따위가 있을 것이다.

물론 그때는 본인은 <날개셋> 한글 입력기 개발에 도움이 안 되는 건 진짜 생까고 무시하던 시절이어서 그 코스의 의미를 제대로 이해를 못 했다. 왜 괜히 계산 과정을 이 따위로 어색하게 표기를 하는지..??
그건 사칙연산 같은 기초적인 연산자조차도 통상적인 표기법이나 우선순위를 깡그리 무시하고 정말로 오로지 함수 위주로.. 프로그래밍이, 아니 계산(computing)이라는 작업 자체를 몽땅 주어진 규칙대로 피연산자들을 처리해서 reduce하는 과정이라고 극도로 추상화한 귀결일 것이다. 일종의 발상의 전환인 것이다. car, cdr 명령이 튜링 기계로 치면 메모리 셀을 이동하고 값을 읽는 동작에 해당할 것이다.

단, Scheme도 마냥 순수 함수형 언어이기만 한 것은 아니다. 필요한 경우 대입 연산이 있을 수 있고 일부 절차형 패러다임 구문을 집어넣을 수도 있다. 마치 C#에서 부분적으로 unsafe, unmanaged 코드를 집어넣듯이 말이다.
그리고 반대로 C++ 역시, 기본적으로 객체지향 패러다임을 주류로 내세운 절차형 언어이지만 최근에는 함수형 프로그래밍 패러다임도 받아들여서 람다 함수를 기존 함수 포인터나 functor의 대용으로 쓸 수 있게 되었듯이.. 요즘 언어들의 대세는 자기 정체성은 유지하면서 다른 패러다임에서도 유용한 건 적극 받아들이는 것인 듯하다.

과연 함수형 프로그래밍 언어가 그저 대학교 과목에서나 잠깐 접하고 마는 떡밥 내지 PL 분야의 연구자들만 쓰는 도구 수준을 넘어.. 현업에서 적극 즐겨 쓰이는 날이 올지 모르겠다. 지금 현업에서 전혀 안 쓰인다는 말은 아니지만 아직까지는 수학 덕후, 컴덕후들의 전유물이라는 인상이 강한 편이니 말이다. 그래도 한 가지 확실한 건, 함수형 프로그래밍 패러다임을 실현해도 될 정도로 요즘 컴터 환경이 좋아지자, 각종 언어들에도 이 패러다임이 적극 많이 도입되고 이게 유행을 타고 있다는 사실이다.

여담으로, 람다 대수를 고안한 앨론조 처치는 family name이 어째 '교회'다..;; 독실한 신자 가문이기라도 했나 싶은 잡생각이 든다.

그리고 궁금한 게 있는데.. 이름 없는 함수에서 재귀호출을 해야 할 때 함수 자기 자신을 가리키는 this, self 같은 키워드는 없는가 싶다.
이 의문은 C++에서 람다 함수가 추가되었을 때부터 여러 프로그래머들에 의해 제기되어 왔다. 하지만 뾰족한 해결책은 없으며, std::function에다가 자신을 저장한 뒤, 그 변수명을 캡처로 도로 넘겨 줘야만 재귀호출이 가능하다. Scheme 역시 일단 def로 자기 이름을 지은 뒤, 그 이름을 호출해 줘야 된다.

재귀호출을 그렇게도 좋아하는 함수형 언어가

[](int x) { return x<=1 ? 1: this_func_itself(x)*(x-1); }

개념적으로 this_func_itself에 해당하는 키워드 같은 건 정말 없는 건지 신기한 노릇이 아닐 수 없다.

Posted by 사무엘

2014/12/28 08:39 2014/12/28 08:39
, , ,
Response
No Trackback , 9 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1044

예전에도 몇 차례 얘기했듯이 비주얼 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

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

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2019/09   »
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:
1252675
Today:
338
Yesterday:
441