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

컴퓨터 프로그램에서 실행 제어라 하면 조건과 분기, 반복, 예외 처리 같은 것들이 있는데.. 절차형 프로그래밍 언어에서는 이런 게 예약어와 블록 구조 같은 걸로 표현되고, 단순 함수 호출이나 연산은 위에서부터 아래로 순차적으로 수행되는 편이다.

그런데 C 언어는 타 언어었으면 예약어를 써서 구현되었을 실행 흐름 제어도 다 함수로 구현되는 경우가 종종 있다. 몇 가지 예를 들면 이렇다. 코드 정적 분석 프로그램 같은 걸 만든다면 이런 건 함수 차원에서 예외적으로 다뤄져야 한다.
C가 저수준이라는 소리를 괜히 듣는 게 아닌 듯하다.

1. signal

시스템 차원에서 인터럽트가 발생했을 때 실행될 콜백 함수를 지정한다. Ctrl+C나 Ctrl+Break가 눌리는 것도 이런 상황에 포함되나, Windows의 경우 C 표준을 준수하느라 함수는 동일해도 Ctrl 키 인터럽트는 다른 인터럽트와는 꽤 다른 방식으로 따로 처리된다. (Windows API에 SetConsoleCtrlHandler이라는 함수가 있음) 사실, Windows는 자체적인 예외 처리 함수 지정 메커니즘도 제공한다.
현대의 언어라면 다 try ... catch로 처리했을 사항들이다. SIG* 상수들은 catch 구문에다 별도의 값이나 타입으로 전달되고 말이다.

2. setjmp/longjmp

C 언어에 이런 함수도 존재한다는 걸 처음 알았을 때 굉장히 놀랐었다. goto는 한 함수 안에서만 분기가 가능하지만 얘는 아예 함수의 경계를 초월하여 이전의 setjmp 실행 직후 상황으로 분기를 시켜 주기 때문이다. 이 함수는 다음과 같이 사용하면 된다. 개념적으로 운영체제의 '시스템 복원'을 생각해도 쉽게 이해할 수 있다.

#include <setjmp.h>
jmp_buf jb;

void Func(int n)
{
    printf("%d\n", n);
    if(n==5) longjmp(jb, 0); else Func(n+1);
}

int main()
{
    if(setjmp(jb)) {
        puts("recursion interrupted.");
    }
    else {
        puts("OK, try");
        Func(0);
    }
    return 0;
}

jmp_buf라는 버퍼 자료형을 선언한다. 얘는 배열에 대한 typedef이며, 함수의 인자로 전달될 때는 자동으로 포인터처럼 취급된다. 그렇기 때문에 setjmp, longjmp의 인자로 전달할 때 &를 붙일 필요가 없으며, 그리고 안 붙이더라도 언제나 내부 컨텐츠는 call by reference처럼 취급된다. jb는 매번 함수의 인자로 전달할 게 아니라면 그 특성상 전역변수로 선언해 놓는 게 속 편하다.

그럼, setjmp를 호출하여 되돌아가고 싶은 지점에 대한 스냅샷을 만든다. 스냅샷을 만든 직후에는 setjmp의 리턴값이 0인 것으로 간주된다. 그래서 위의 코드에서는 "OK, try"가 먼저 출력되고 Func가 호출된다.

나중에 Func가 굉장히 복잡하게 실행된 뒤에 이것들을 몽땅 한 큐에 종료해야겠다 싶으면 longjmp를 호출한다. 그러면 얘는 아까 setjmp를 호출한 곳에서 함수가 0이 아닌 값이 리턴된 상황으로 모든 컨텍스트가 '원상복귀' 된다. 그래서 "recursion interrupted"가 출력되고 실행이 끝난다.

구체적인 리턴값은 longjmp의 인자에다가 줄 수 있다. 다만, 여기에다가 0을 지정하면 setjmp가 처음 호출되어 0이 리턴된 것과 구분이 되지 않기 때문에 setjmp의 리턴값이 1인 것으로 값이 일부러 보정된다.

위의 코드는 예외 처리 구문을 사용한 다음 코드와 실행 결과가 완전히 동일하다. 이번에도 try, catch가 답이다. 언어 차원에서 예약어를 동원해서 구현했을 기능이 그냥 함수로 처리되어 있다는 얘기가 바로 이런 의미이다.

void Func(int n)
{
    printf("%d\n", n);
    if(n==5) throw 1; else Func(n+1);
}

int main()
{
    try {
        puts("OK, try");
        Func(0);
    }
    catch(int e) {
        puts("recursion interrupted.");
    }
}

setjmp/longjmp는 언어 차원에서 제공되는 기능이 아니다 보니, 저렇게 함수들을 이탈할 때 C++ 객체들의 소멸자 함수 처리가 제대로 되지 않는다는 한계도 있다. 가변 인자만큼이나 C와 C++의 기능이 서로 충돌하는 지점이다.
그래도 얘는 시스템 프로그래밍 차원에서 고유한 용도가 있다 보니, 이들 함수가 현대의 컴파일러에서 deprecate됐다거나, 뭔가 기능이 보강된 *_s 버전이 생겼다거나 하지는 않다.

3. fork

새로운 실행 주체를 생성하는 함수라는 점에서 Windows의 CreateProcess나 CreateThread와 얼추 비슷하다. 그러나 생성하는 방식은 완전히 다르다.
Windows에서는 프로세스를 생성할 때 파일명을 주며, 그 프로세스는 완전히 처음부터 다시 실행된다. 그리고 스레드는 콜백 함수를 지정해서 생성하며, 그 콜백 함수의 실행이 끝나면 스레드 역시 종료되어 사라진다.

그러나 fork는 지금 나 자신과 메모리 구조와 스택 프레임, 내부 상태 문맥 같은 게 완~전히 동일한 프로세스가 하나 또 실행된다. 그래서 fork를 처음 호출한 기존 프로세스는 fork의 리턴값이 nonzero인 것으로 간주되어 실행이 계속되며, 새로 생성된 프로세스는 리턴값이 0인 것처럼 간주되어 실행이 계속된다. 굉장히 신기한 결과인데, 함수의 디자인 방식이 setjmp와 미묘하게 비슷하다고 볼 수도 있는지는 잘 모르겠다.

//공통 처리 진행 후,
if(fork()==0) {
    //분기된 자식 프로세스 문맥. 하지만 공통 부분에서 만들어 뒀던 변수들에 접근 가능함.
}
else {
    //'공통'을 실행하던 부모 프로세스 문맥
}

(뭐, 정확히는 실행이 성공하면 양수가 돌아오고, 실패하면 음수가 돌아오니 이건 마치 GetMessage의 리턴값만큼이나 주의할 필요는 있다.)

Windows API에는 저렇게 모든 실행 문맥을 그대로 복제해서 자신의 분신 프로세스를 만드는 함수가 존재하지 않는다. 프로그램들 내부에 포인터들까지 있다는 점까지 감안하면 정말 주소 공간이 문자 단위로 정확하게 일치해야 할 텐데, 그걸 그대로 복제하는 건 성능 오버헤드가 크지 않겠나 하는 생각도 든다.

fork는 프로세스를 생성하는 놈이다 보니 CreateProcess와 마찬가지로 비동기적으로 실행된다. 앞서 소개한 signal도 인터럽트 함수가 이론적으로 비동기적으로 실행될 수 있다. set/longjmp는 하는 일은 기괴해도 그래도 프로세스/스레드를 넘나드는 물건은 아니니 대조적이다.

그래서 signal 핸들러나 fork를 사용하는 코드에서는 주의해야 할 점이 있는데, 버퍼를 사용하는 고수준 IO 함수를 사용해서는 안 된다. 쉽게 말해 Hello, world를 찍을 때도 간단하게 printf나 puts를 쓰지 말고 write(1, "Hello", 5)라고 좀 번거로운 방법을 써야 한다. 비동기적인 환경에서 여러 실행 단위가 고수준 IO에 동시에 접근하면 출력이 꼬일 수 있기 때문이다.

먼 옛날 대학 시절, 시스템 프로그래밍 숙제를 하던 시절에 어떤 수강생이 뭘 잘못 건드렸는지 자식 프로세스를 무한 생성하는 삽질을 했고, 이 때문에 학과 서버가 몽땅 다운되어서 수강생들이 과제를 할 수가 없어지는 사태가 벌어졌다. 이 정도면 그 학생뿐만 아니라 계정별로 자원 할당 한계 관리를 제대로 하지 않은 서버 관리자에게도 책임이 있지 않나 생각이 든다만, 어쨌든 이것 때문에 과제의 듀(제출 기한)까지 불가피하게 연장된 적이 있었다.

이것 때문에 빡친 모 친구의 메신저 대화명은 "포크 삽질하는 놈 포크로 찍어 버린다 -_-"였던 것을 본인은 지금도 기억하고 있다.

Posted by 사무엘

2016/07/21 08:39 2016/07/21 08:39
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1252

C++ 클래스에서 타입이 char나 int 같은 정수형 스칼라(배열이 아닌)인 static const 멤버는 선언 후에 별도의 정의를 따로 안 해 줘도 된다. 선언과 동시에 값을 지정할 수 있다. 본인은 이에 대해서 3년 전에도 한번 글을 썼던 적이 있다.

class foo {
public:
    static const int bar = 500;
};

하지만 멤버가 char나 int 같은 간단한(primitive) 타입이 아니고 다른 구조체이거나, 혹은 간단한 타입이라도 배열이라면 얘기가 달라진다. 아까처럼 즉석 초기화를 할 수 없으며, 반드시 정의를 해 줘야 된다.

//헤더 파일
class foo {
public:
    static const POINT bar1;
    static const int bar2[];
};

//소스 파일
const foo::bar1 = { 1024, 768 }; // .x, .y
const foo::bar2[] = { 1, 2, 100 }; // [0], [1], [2]

자, 여기까지는 뭐 당연한 사실이다. 그런데 얼마 전엔 회사에서 이와 관련된 기괴한 일을 하나 겪어서 이곳에다 소개하고자 한다.
static const int 형태로 상수를 하나 추가해서 사용했는데 컴파일러가 이걸 도무지 인식을 못 하고 링크 에러를 내는 것이었다. global scope에 선언된 const 야 C++에서는 static이 디폴트이기 때문에 extern을 명시적으로 지정해야 한다지만, 멀쩡한 C++의 static const 멤버를 왜 인식을 못 하지?
게다가 더 이상한 건 Visual C++과 xcode는 다 문제 없는데 안드로이드의 빌드 환경인 gcc만 저런다는 것이었다.

멀쩡한 코드를 고치고 재빌드 하면서 잠시 헤매긴 했지만, 문제 자체는 구글링을 통해 원인을 찾아서 곧 해결할 수 있었다.
답부터 말하자면, 저 위의 bar는 선언과 초기값 지정이 되어 있음에도 불구하고 const int foo::bar1; 이라고 몸체도 소스 코드 어딘가에 정의해 줘야 했다. 단, 500이라고 초기값을 지정하는 건 선언부와 정의부 중 한 곳에다가만 하면 된다. 마치 함수의 디폴트 인자를 선언부와 정의부 중 한 곳에만 주면 되듯이.

소스 코드에서 어떤 숫자에다가 명칭을 부여하기 위해서는 enum이나 전처리기 #define을 사용할 수 있다. 이에 비해 const는 위의 두 방법과는 달리 명시적인 타입을 가지며, & 연산자를 이용해서 값의 주소도 얻을 수 있다는 차이가 있다.

static const 멤버를 R-value로만 쓰는 것은 그때 그때 그 숫자를 말 그대로 상수 형태로 집어넣는 것과 같다. 그러니 enum이나 #define과 별 차이가 없으며, 굳이 이 변수의 실체(= 주소)가 없어도 된다.
그러나 이 값을 그대로 파일에다 쓴다거나 할 때는 값이 담긴 메모리 주소를 줘야 한다. 그리고 C++의 템플릿 라이브러리 중에도 일단 value가 아니라 참조자가 전달되는 함수에다가 static const 멤버를 넘겨주면 그 멤버의 주소가 필요해진다.

그리고 그렇게 값이 아니라 주소가 필요한 경우가 있다면, gcc는 비록 선언부에서 값이 지정된 static const 멤버라 해도 별도의 몸체의 정의가 있어야만 링크를 제대로 수행해 줬다. 이니셜라이저가 있는 것도 아니고, 그냥 선언부에 있는 변수를 거의 그대로 다시 써 주는 잉여일 뿐인데도 그게 꼭 필요했다. 그 반면 Visual C++ 등 타 컴파일러는 몸체 정의가 있건 없건 결과는 동일하게 나왔다. 어째 이런 차이가 존재할 수 있는 걸까?

한편으로 gcc는, 주소만 요구하지 않는다면 구조체나 배열까지는 아니어도 float, double 같은 부동소수점 스칼라 상수를 몸체 정의 없이 static const로 선언하는 것도 허용해 줬다.

class foo {
public:
    static const double bar4 = 0.0025;
};

이거야말로 비표준이며 일단 Visual C++ 등 여타 컴파일러에서는 허용되지 않음에도 불구하고 gcc는 이를 지원한다. 혹시 나중에 표준으로 등재됐다면 댓글로 알려 주시기 바람. 요즘 C++은 하루가 멀다 하고 급변하고 있어서.. 과거 C++이 98과 03 버전을 거친 뒤 갑자기 1x대부터 확 바뀌기 시작했는데, 이는 마치 마소가 2000년대에 닷넷 때문에 C++ 지원을 등한시하다가 2010년대부터 트렌드가 바뀐 것과도 분위기가 일치하는 것 같다.

enum은 정수형 타입만 지원하기 때문에 실수 상수를 정의하는 건 #define 아니면 const밖에 선택의 여지가 없는데 gcc처럼 할 수 있다면 편리할 것 같다.
다만 &foo::bar4가 필요하다면 gcc라도 물론 const double foo::bar4; 라는 몸체 선언이 추가로 필요해진다.
아무튼, static const 멤버와 관련하여 gcc의 특이한 면모를 다시 생각할 수 있는 시간이었다.

Posted by 사무엘

2016/04/14 08:39 2016/04/14 08:39
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1214

C/C++에서 대괄호 [ ]는 일단 배열과 관련이 있는 문자이다.
타입을 선언하는 문맥에서 a[...]라고 하면 a가 배열임을 나타내고 선언한다. [] 안에는 배열의 크기를 나타내는 정수 상수(= 컴파일 타임 때 바로 값이 결정되는)만이 올 수 있다.
그 반면 값을 계산하는 문맥에서 a[...]는 일단은 배열이나 포인터를 역참조하는 *(a+...)와 동치가 되기 때문에, 상수가 아닌 임의의 변수값이 들어올 수 있다. 게다가 C++에서는 []를 연산자로서 오버로드도 할 수 있으므로 정수가 아닌 다른 타입의 값이 들어오는 게 가능하다.

그럼 속에 아무것도 없이 말 그대로 []만 달랑 오는 건 언제 가능할까?
일단 값을 계산하는 실행 문맥에서는 전혀 가능하지 않다. 함수 호출이 아닌 일반 수식에서 일반 괄호가 ()요렇게만 달랑 올 수가 없는 것과 같은 이치이다. 또한 인자가 전혀 없는 형태로 [] 연산자 함수를 오버로딩 하는 것도 안 된다.
얘가 쓰이는 가장 유용하고 편리하고 직관적인 상황은, 이니셜라이저로부터 크기를 자동으로 유추할 수 있는 배열을 선언하여 곧장 초기화할 때이다.

char pt[] = "Hello"; //null 문자까지 합해서 6이라는 크기가 자동으로 유추됨.
static const int samp[] = {1, 5, 7, 8}; //4라는 크기가 자동으로 유추됨.

이 정도야 []는 명백하게 배열을 나타내며, 그 의미를 충분히 납득할 수 있다. 헷갈릴 게 없다.
그리고 지역변수 말고 다른 번역 단위에 있는 배열을 요런 게 있다고 선언만 해 놓는 문맥일 때도 구체적인 크기가 생략된 arr[] 같은 구문이 올 수 있다. 이 경우 클래스로 치면 forward 선언과 얼추 비슷한 역할을 한다. 배열을 참조하는 코드를 생성할 수는 있지만 sizeof 연산자는 쓸 수 없게 된다. 온전한 배열은 아니지만 포인터도 아니기 때문에 여기에 다른 임의의 주소를 대입할 수는 없다.

그런데 []는 일부 제한된 문맥과 범위에서는 그냥 포인터를 나타낼 때도 있다. 이거 다시 보니 굉장히 요물스럽게 느껴진다.

void foo(char ptr[]);
void foo(char *p);

위와 아래의 함수 선언은 완전히 동치이다. 그렇기 때문에 저 두 쌍으로는 함수 오버로딩을 할 수 없다.
이게 요물스럽게 느껴지는 이유는, (1) 첫째, 함수의 인자에서만 사용 가능한 문법이기 때문이다.
일반 변수를 선언할 때야 char ptr[]은 포인터가 아니라 배열로 인식된다. 그렇기 때문에 이니셜라이저 없이 저렇게만 달랑 써 놓으면 크기를 알 수 없다고 컴파일 에러가 난다.
그에 반해 함수 인자에서는 배열을 선언하거나 초기화 따위 할 일이 없으니 ptr[]은 *p와 동일하게 인식된다.

우리는 프로그램으로부터 명령 인자를 받기 위해 main 함수로부터 argc, argv 인자를 살펴본다. 그런데 정말 이상한 건 argv는 거의 언제나 관행적으로 *argv[]라고 선언해 놓고 써 왔다는 것이다. 이건 알고 보면 **argv와 본질적으로 완전히 동치일 뿐인데도 말이다. 하지만 *argv[]와 **argv는 왠지 느낌상 서로 동일해 보이지가 않는다.

함수 인자와는 달리 리턴값은 char *bar()만 가능하지 char []bar 이렇게 쓸 수 없다. 함수 인자 말고는 리턴값이나 지역 변수 선언, 형변환 연산자, typedef 등 타입을 명시하는 그 어떤 문맥에서도 []를 저런 식으로 쓰는 건 문법적으로 가능하지 않다.

(2) 둘째, []는 그렇다고 해서 함수 인자이기만 하면 *와 완전히 등가 교환이 가능한 것도 아니다.
[]의 지원 범위는 오로지 1중 포인터 한정이다. 2중 포인터인 char **a를 a[][] 따위로 대체할 수는 없으며 오로지 한 단계만 *a[]로 대체할 수 있을 뿐이다. 한 단계 포인터를 대체할 수 있다는 점에서는 C++의 참조자 &를 살짝 닮은 것 같기도 하다.

그럼 함수 인자에서는 왜 저걸 예외적으로 가능하게 해 놓은 것일까?
정확한 이유는 알 수 없지만 C/C++은 배열은 함수 인자로 전달할 때 값이 새로 복제되는 게 아니라, 언제나 주소(= 포인터) 형태로만 전달한다는 점을 부각시키기 위해서 저렇게 한 게 아닌가 싶다. N차원 배열은 언제나 N-1차원 배열의 1중 포인터가 된단 얘기다. 다차원 배열을 선언해서 함수로 전달하는 상황을 생각해 보자.

int table[][2][4]={
    {{1,2,3,4}, {3,4,5,6}}, {{2,6,2,3}, {5,6,3,4}}, {{1,}, {2,}}
};

foo(table);

사실 C/C++은 엄밀히 말하면 다차원 배열이라는 것도 없고 그저 '배열의 배열'이 있을 뿐이다.
2*4 배열의 배열을 선언한다는 건 생략할 수 없고, 유일하게 생략할 수 있는 건 제일 겉에 있는 최종 원소의 개수(저기서는 3)이다.
이런 3차원 배열을 인자로 받는 함수는 프로토타입을 어떻게 만들면 좋을까?

void foo(int a[][2][4]);

table은 함수의 인자로 전달할 때는 그대로 2*4 배열의 포인터가 되고, 그걸 함수 인자로 표현하는 걸 배열 변수를 선언할 때와 동일하게 [][2][4]로 할 수가 있다. 함수 인자에서 []의 의미는 바로 여기에 있다고 보는 게 타당할 것이다. 함수 인자에서도 [] 안에 숫자를 생략할 수 있는 건 제일 겉의 차원수 하나뿐이다.

위의 함수의 인자는 개념적으로 아래와 완전히 동일하다. *(a+2)와 a[2]가 동일한 것만큼이나 서로 동일하다.

void Fune(int (*a)[2][4]);

a[][2][4]를 사용하지 않았으면 그냥 포인터나 다중 포인터가 아니라 '배열의 포인터'를 표현하기 위해 저렇게 괄호를 쳐야 했을 텐데 [] 표기는 괄호가 또 들어갈 필요를 없애 주고 표현을 간결하게 만들어 준다.
배열 선언 table[][2][4]에서는 []는 원소 개수가 이니셜라이저에 명시돼 있다는 뜻이고, 함수 인자 a[][2][4]에서는 이게 포인터라는 뜻이라니..;;

이건 무슨 최신 C++ 기능도 아니고 C의 완전 기초 필수 아이템인데도... 본인조차 []의 의미와 용도에 대해서 제대로 진지하게 생각을 안 했던 것 같다. 신기하기 그지없다.

Posted by 사무엘

2016/02/04 08:27 2016/02/04 08:27
,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1189

몇 가지 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

C/C++ 같은 지극히 static한 컴파일 지향 언어에서는 상상도 못 할 일이겠지만, 아주 다이나믹하고 인터프리터를 지향하는 프로그래밍 언어들은 문자열에 담긴 자기 언어 코드를 지금의 변수/함수 context를 기준으로 실행해 주는... 엄청난 기능을 제공하기도 한다.

PHP와 자바스크립트 모두 eval이라는 함수가 이 일을 한다. 루비던가 펄이던가 문자열의 동적 처리가 강한 다른 언어에도 응당 동일 기능이 있다.
문자열 변수에 들어있는 값(가령 "abc")을 통해서 해당 이름의 변수에 접근하는 것(abc)도 가능하다. C/C++에서는 지역 변수는 그냥 함수 스택 프레임으로부터 고정된 오프셋 숫자를 나타내겠지만, 저런 언어에서 지역 변수는 힙 기반의 해시나 트리를 참조하는 유동적인 이름-값 key 명칭에 해당할 것이다.

그리고 또 다르게 비유하자면, static type 언어는 테이블의 각 필드별로 타입과 정보량이 매우 엄격하게 정해져야 하는 데이터베이스와 같고(액세스),
dynamic type 언어는 셀에 들어가는 값과 타입이 자유자재로 바뀌어도 되는 스프레드 시트와 같다(엑셀).
어마어마한 대용량의 데이터를 처리하는 성능은 까다롭고 전문적인 DB가 훨씬 더 뛰어나지만, 그래도 엑셀 정도만 돼도 성능이 굉장히 좋아지고 그러면서 일상생활에서는 더 편리하다. 세상엔 그 정도 tradeoff는 어디에나 있는 듯하다.

그나저나, 문자열 코드를 실행하는 기능은 편리한 건 그렇다 치더라도 보안을 매우 취약하게 만들 수 있다는 점을 감안하고 사용해야 한다. C 언어의 %d, %s 같은 포맷 문자열만 해도 이미 위험하기 때문에 입출력 포맷 문자열에다가는 사용자로부터 실시간으로 입력받은 문자열을 절대 넣지 말아야 한다는 것이 불문율이다. 그런데 하물며 저건 문자열에 명시된 대로 아예 프로그램이 실행되니 C의 포맷 문자열과는 차원이 다른 방식으로 위험할 수밖에 없다.

본인이 경험해 본 프로그래밍 언어 중에 코드를 실시간으로 생성하고 자기 자신을 변형할 수 있는 물건의 원조는 역시 GWBASIC이었다.
비록 얘는 문자열을 코드로 그대로 해석하고 실행하는 기능은 없지만, 그래도 RUN, CHAIN, MERGE처럼 파일 차원에서 임의의 코드를 즉석에서 실행하는 기능은 있었기 때문이다.

다음은 스택 기반의 복잡한 파싱 없이 15*(2+5), 256^2, 1/5 등의 수식을 쓱쓱 계산해 내는 계산기 프로그램이다. 내가 이걸 생각한 게 20여 년 전의 일이다. ㅎㅎ

10 INPUT "Expression? ", A$
20 IF A$="" THEN END
30 OPEN "TMP.BAS" FOR OUTPUT AS #1
40 PRINT #1, "10 ON ERROR GOTO 30"
50 PRINT #1, "20 PRINT "+A$+": GOTO 40"
60 PRINT #1, "30 PRINT "+CHR$(34)+"Error"+CHR$(34)
70 PRINT #1, "40 RUN "+CHR$(34)+"CALC.BAS"+CHR$(34)
80 CLOSE #1
90 RUN "TMP.BAS"

프로그램의 동작 원리를 알면 피식 웃음이 나올 것이다. 이 프로그램이 하는 일이라고는 입력받은 수식을 그대로 PRINT하는 프로그램을 생성한 뒤, 그걸 실행하는 것이 전부이기 때문이다. 즉, 실질적인 계산을 하는 부분은 내가 짠 코드가 아니라 GWBASIC 인터프리터 자체인 것이다.
그나마 수식에 오류가 있어도 뻗지 않게 하려고 ON ERROR GOTO를 쓰는 일말의 치밀함(?)을 보였다.

사실, RUN은 지금 내 프로그램을 지우고 실행 제어를 다른 프로그램으로 완전히 옮기는 명령이다. MERGE나 CHAIN을 쓰면 내 프로그램의 컨텍스트를 유지하면서 타 프로그램을 그대로 병합을 할 수가 있으며 이게 내가 의도한 형태에 더 가깝다. 하지만 본인은 저 두 명령은 어떻게 사용하는지 잘 모른다.

MERGE 같은 경우 QuickBasic이나 QBasic으로 넘어가면서 후대의 베이직 언어들도 지원하지 않게 되었으며, 후대의 언어들도 차라리 문자열 코드를 실행하는 eval은 지원해도 저런 무지막지한 명령을 지원하지는 않는다. 먼 옛날에 컴퓨터와 함께 제공되었던 두툼한 MS-DOS 겸 GWBASIC 매뉴얼에서나 그런 명령에 대한 자세한 설명을 볼 수 있었던 걸로 기억한다.

자, 그럼 다시 자바스크립트 얘기로 돌아온다. 얘는 HTML, CSS와 더불어 웹을 구성하는 한 축이며, HTML 문서가 MS 오피스의 Word, Excel 같은 일반 문서라면 자바스크립트는 그런 문서에 첨부된 매크로나 마찬가지이다. 다른 모든 언어들은 사용하기 위해서 해당 언어 구현체들 런타임이나 엔진을 설치해야 하지만 자바스크립트는 웹브라우저만 있는 운영체제라면 바로 돌려볼 수 있다는 차이가 존재한다.

자바스크립트에는 딱히 컴파일· 링크 같은 건 없지만 난독화 겸 간소화(compaction)라는 리팩터링 후처리를 거쳐서 서버에 올라가곤 한다. 굳이 IOCCC 같은 대회를 노려서는 아니다. 핵심 알고리즘의 누출을 막고(분석을 완전히 막지 못하더라도, 어렵게 만들거나 지연시키기 위해) 크기도 줄이기 위해서이다.

난독화 내지 간소화를 자동으로 해 주는 도구가 당연히 존재한다. 모든 주석이나 공란은 제거되며 지역 변수는 전부 a~z, aa ... 처럼 식별만 가능하지 최대한 짤막하고 암호 같은 명칭으로 바뀐다. 상수 명칭의 조합으로 좀 더 알아보기 쉽게 숫자를 표현하던 것도 당연히 다 실제 숫자로 치환된다.

그런데 자바스크립트를 리팩터링해 주는 툴 중에는 그런 명칭 치환 수준을 넘어서 코드를 암호화에 가까운 완전히 다른 형태로 바꿔 버리는 것도 있다. 그런 덩어리를 보면 예전 코드는 다 이상한 문자열로 바뀌고 코드의 가장 바깥은 eval을 호출하는 형태가 돼 있다. 그 문자열을 원래의 자바스크립트 코드로 해독하는 건 다른 치환과 디코딩 함수들이고 말이다.

이건 개념적으로 실행 파일 압축과 동일한 기법이다. 거기도 압축을 푸는 데 필요한 최소한의 코드만 들어있고 나머지는 데이터를 압축을 풀어서 코드를 생성함으로써 실행되니까 말이다. 그게 기계어 코드가 아닌 인터프리터 스크립트형 언어에도 저런 식으로 존재할 수 있다는 게 신기하게 느껴진다. 자바스크립트의 JIT는 저런 것까지 다 감안해서 동작해야 할 테니 런타임이 안 그래도 거의 VM 수준의 스케일로 만들어져야 할 듯하다.

이념적으로 C/C++의 완전 정반대편에 있는 동적 언어를 익혀서 나쁠 건 없는 것 같다. JS는 그렇다 치더라도 파이썬은 슬라이싱 기능이 완전 마음에 든다. 늘 하는 생각이지만, 메모리 관리가 자동으로 되고 마음대로 new를 남발해도 되는 언어로 코딩을 하는 기분은 운전으로 치면 정말 클러치 걱정을 할 필요 없는 자동변속기 차를 모는 것과도 같아 보인다.

그리고 저런 동적 언어들은 아무래도 문자열 처리가 강세이며, 언어의 설계자가 확실히 정규 표현식 덕후라는 생각이 들었다.
메모리 관리를 할 필요가 없는 건 편하지만 C/C++의 포인터처럼 아주 저수준 직관적으로 문자열에 접근을 할 수 없는 건 좀 불편하다. 새로운 언어를 익히면 그 언어의 String 클래스가 제공하는 멤버 함수(메소드)부터 새로 익혀야 하니까.

또한 자바스크립트, 정규 표현식, URL, 그리고 HTML 내부에 제각각으로 돌아가는 탈출문자들 때문에 신물이 났다.
문자열을 검색· 치환하는 함수들이 다 있는 그대로 찾아 바꾸는 게 아니라 기본적으로 정규 표현식 기반이다. 역슬래시나 따옴표 같은 크리티컬한 문자 그 자체를 찾거나 그걸로 바꿔야 할 때 프로그램이 곧이곧대로 동작하지 않아서 그것도 좀 애로사항이었다.

Posted by 사무엘

2015/04/16 19:36 2015/04/16 19:36
,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1085

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

하지만 메모리 할당은 그렇지 않다. 수~수십 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

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

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/05   »
      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:
2695844
Today:
279
Yesterday:
1596