dev.log2004. 2. 27. 12:28
C로 프로그래밍을 처음 배울때 누구나 만들어본 프로그램이 있다. 모두들 짐작할 것이다. 바로 그 유명한 "Hello, world" 프로그램이다. 이 프로그램은 원래 커니건 & 리치가 쓴 The C Programming Language에 등장하는 첫번째 예제로 그후 거의 모든 C입문서의 첫번째 예제로 쓰이고 있다. Hello, world 프로그램은 다음과 같은 간단한 코드이다.

#inlcude <stdio.h>

void main()
{
    printf( "Hello, world\n" );
}

이 예제에서는 Hello, world를 출력하기 위해 printf()를 사용하는데, 아는 사람은 알겠지만, printf는 C의 표준 라이브러리 함수중 가장 복잡한 함수이다. printf()의 모든 기능을 완전히 이해하기 위해서는 variable argument와 호출규약 등 언어의 지저분한(?) 하위레벨 기능까지를 알아야 하는 까닭이다. C에서 가장 처음 배우는 예제가 C에서 가장 복잡한 함수를 사용한다는 것은 일종의 아이러니가 아닐까.
사실, 위의 예제에서 문자열을 출력한다는 용도로는 puts()함수를 쓰는 것이 더 적합할 것이다. 그러나 아마도 커니건 & 리치는 출력에 관한한 거의 만능에 가까운 printf()함수를, 사용자에게 친숙하게 만들고자 일부러 썼을 것이다.
C라는 언어안에서만 이야기 한다면, printf()의 가장 큰 약점은 수행속도이다. printf()는 첫번째 인자로 주어지는 형식지정문자열(format specifier)을 파싱하여 그 뒤에 주어지는 인자의 출력형식을 결정하는데, 여기에 걸리는 오버헤드가 만만치 않다. 간단한 문자열 출력이라면 puts등을 사용하는 것이 적절하다. 물론 문자열, 정수, 부동소수 등이 복잡하게 얽힌 형식으로 출력한다면 이야기가 달라지긴 하지만.

그러면 이제 논의를 C++의 세계로 옮겨 보자.
C++의 표준 출력기능은 이제 printf[함수]가 아닌 cout[객체]로 옮겨졌다. 왜 옮겼을까?
printf()가 가지는 가장 큰 약점은 형안정성을 보증하지 못한다는 것이다.

#inlcude <stdio.h>

void main()
{
    printf( "%f", 1 );
}

위 와 같은 코드는 C환경에서 정상적으로 컴파일되며 실행도 된다. 물론 현대적인 컴파일러나, lint같은 툴을 쓴다면 printf()에 주어진 형식지정자와 실제인자의 타입이 일치하지 않는다는 워닝을 내줄 수 있겠지만, 위의 코드가 작동하며, 또한 언뜻 보기에 의미 없는 값을 출력한다는 점은 변하지 않는다.
이는 printf()함수가 실매개변수의 타입과 출력 형식을 별도의 정보로 지정하기 때문이다. 대개의 출력형식은 실매개변수의 타입에 따라 결정된다. int형의 실매개변수는 거의 90%이상 %d형식으로 출력할 것이며, 절대 %f형식으로 출력하려고 하는 사람은 없을 것이다. 이는 타입정보와 형식정보가 실제로는 별개의 정보가 아니며 둘 사이에 상관관계가 있다는 것을 의미한다. 물론 고의적으로 플로팅포인트 타입의 비트패턴을 보기 위해 float타입을 16진수 정수형으로 출력하는 경우도 있으나 대개의 경우 타입에 따라 출력 형식이 정해짐을 주지해야 한다. 그러나 printf()는 밀접한 상관관계가 있는 두개의 정보를 별개의 정보로 지정함으로써 둘 사이에 미스매치가 발생할 경우 그에 대해 어떠한 식별도 할 수 없다는 맹점을 지니고 있다.
C++에서 가장 강화된 것 중의 하나가 타입시스템이라고 말한바 있다. 따라서 C++의 설계당시부터 표준출력을 담당하는 printf()함수의 위와같은 맹점때문에 C++에서 표준출력을 담당할 객체가 cout으로 정해지게 된다.
#inlcude <iostream>

int main()
{
    std::cout << "Hello, world" << std::endl;
    return 0;
}

첫번째 Hello, world와 같은 일을 하는 프로그램의 C++버전이다.
cout 은 printf()와는 달리 형 안정성을 보장한다. cout객체가 작동하는 방식은 연산자 오버로딩에 기초를 두고 있기 때문이다. 무슨 말인고 하니, cout객체가 출력할 수 있는 모든 타입에 대해 <<연산자가 오버로딩 되어 있다는 것이다. 이제 컴파일러가 cout객체와 << 연산자를 통해 출력하라는 문장을 만나면 출력할 대상의 타입에 따라 오버로딩된 <<연산자 중에서 적절한 연산자를 자동으로 골라서 출력해 준다는 의미가 된다. 즉, 타입에 맞는 출력 형식을 컴파일러가 선택하므로 형 안정성이 보장되는 것이다.


#inlcude <iostream>

int main()
{
    std::cout << 1 << 1. << std::endl;
    return 0;
}


위 프로그램은 오해의 여지가 없이 의미있는 결과만을 출력해 낸다. 플로팅포인트 타입을 엉뚱하게 int타입으로 출력한다든가 하는 일은 발생하지 않는다. C++의 표준출력객체 cou은 형식정보와 실제 타입정보를 하나로 묶어 관리함으로써 의도치 않은 동작이 발생하는 것을 미연에 방지할 수 있다.

여기에 더하여 printf()가 가지는 또하나의 맹점은 사용자정의 타입에 대한 지원을 전혀 할 수 없다는 것이다. 어떤 클래스 A의 객체 a가 있을 때, printf( "%a", a )와 같은 식으로 일관성 있는 조작을 할 방법이 전혀 없다. 하지만, cout객체로는 ostream 클래스에 대해 <<연산자만 오버로딩 하면 어떠한 사용자정의 타입이건 내장 타입과 같은 방식으로, 일관성 있게 사용할 수 있다는 점은 매우 중요한 강점이다.


이로써 C++에서 되도록이면 printf()를 쓰지 말아야할 몇가지 이유가 분명해진 셈이다. 물론 몇가지 특수한 상황에서는 printf()류의 함수가 유용할 때가 있으나, 일반적인 출력기능은 이제 cout에게 맡기는 편이 현명하지 않을까.

Posted by uhm
dev.log2004. 2. 13. 02:37
대부분의 C++ 프로그래머들은 예외처리를 잘 쓰지 않는 경향이 있는 것 같다. 그 이유로는 첫째, 그들의 대부분이 C로 코딩을 하다가 C++로 migration한 사람이 대부분이고, 또 둘째로는 C++의 디자인이 자바처럼 예외처리를 장려하지는 않는다는 데 있는 듯 하다.

그래서 예외상황을 처리하기 위해서 대부분의 C++프로그래머들은 전통적인 '리턴값 체크'를 통해 에러를 감지하고 처리한다.

ifstream infile( "some_file_name" );
// .....
if ( infile.fail() )
{
   
// exception handling
    return;
}

대개는 위와 같은 형식이다. 그러나 위와 같은 에러처리를 하다보면 다음과 같은 코드가 컴파일되는 것을 사전에 방지할 도리가 없음을 알게 된다.
ifstream infile( "some_file_name" );
// .....
if ( infile.fail() )
{
    // exception handling..
    infile >> some_string;  // (1)
    // additional exception handling...
    return;
}

(1) 의 코드는 컴파일되는데 아무런 장애가 없다. 정상적으로 만들어진 객체에 정상적으로 선언된 메소드를 정상적으로 호출하는 코드이므로 컴파일러는 아무런 메시지도 내지 않고 컴파일해주게 된다. 그러나 문제는, (1)을 수행하는 시점에서 infile객체는 이미 에러를 내버린 개체라는 점이다. 에러를 처리하면서 에러를 낸 개체를 계속 사용하는 것은 위험한 일이다. 야구공이 실밥이 튿어졌다는 걸 발견하고 그걸 고치려는데 그걸 계속 던지고 놀아야 하겠는가?

C++예외처리의 장점은, try블럭에서 선언된 로컬 개체는 모두 try블럭 안에서만 로컬 스코프를 가진다는 점이다. 위의 코드를 예외처리를 사용하여 try-catch 블럭으로 바꾸면 대충 다음과 같은 꼴을 가질 것이다.

try
{
    ifstream infile( "some_file_name" );
    // .....
    if ( infile.fail() )
        throw ifstream_exception;
}
catch ( ifstream_exception e )
{
    // exception handling..
    infile >> some_string;  // (1)
    // additional exception handling...
    return;
}



위와 다른 점은, (1)의 코드를 컴파일할때 이번에는 컴파일러가 에러메시지를 뱉는다는 점이다. infile객체가 try블럭에서 로컬 스코프를 가지므로 예외가 trow된 순간 infile객체의 수명은 종료되고 catch블럭에서 infile개체에 대한 참조는 컴파일 에러를 내게 된다. 위의 if구문을 통한 에러처리보다 확실히 안전한 방법이라고 할 수 있다.
컴파일러가 한번 점검해 줌으로써 확실히 작동안하는 경우를 배제할 수 있는 방법이 마련된 것이다. 되도록이면 컴파일러에게 많은 일을 시키자.

Posted by uhm
dev.log2004. 2. 3. 14:03
루프는 매우 일반적인 프로그램 제어 구성요소로 어떤 언어든지 루프구조를 지원하고 있다. 심지어는 어셈블리에서조차 LOOP 키워드를 통해서 표현이 가능하다. 루프는 프로그래밍 언어를 처음 배울때 부터 익혀온 워낙 친숙한 개념이라 대부분의 프로그래머는 반복작업을 해야할 때 별 생각없이 루프를 작성하곤 한다. 하지만 조금만 생각을 해보면 어떨까.

가장 흔한 예로, 어떤 배열의 원소를 모두 표준 스트림으로 출력하는 작업을 생각해 보자.
대부분은 다음과 같은 코드를 작성할 것이다.
int a[SIZE] = { 1, 2, 3, 4, 5 }:
for ( int i = 0; i < SIZE; i++ )
print_int( a[i] );


출 력 형태가 아름답지 못한 점은 당분간 신경 쓰지 말고, 코드의 구성방식에 대해서만 생각해 보자. C++에서는 저런 끊임없이 나타나는 단순반복 작업을 위한 알고리즘 템플릿이 마련되어 있다. C++사용자라면 대부분 알고 있겠지만, for_each 알고리듬이 그것이다. for_each를 사용하면 다음과 같이 바뀐다.

int a[SIZE] = { 1, 2, 3, 4, 5 };
for_each( a, a+SIZE, fun_ptr( print_int ) );


for_each 는 말 그대로, 시작 이터레이터로부터 마지막 이터레이터까지 모든 구성요소에 대해 주어진 작업을 수행하는 알고리듬을 구현한 함수이다. 세번째 인자에는 fun_ptr 헬퍼 펑션을 사용하여 print_int 함수를 함수어댑터 객체로 포장하여 사용하였다.
그 런데, 분명히 for_each는 '이터레이터'를 받아들인다고 하였는데, 여기서는 배열이름, 즉 포인터를 사용하였다. 이상하다고 생각하는 독자가 있다면, for_each 알고리듬은 템플릿이라는 점을 다시한번 상기하기 바란다. for_each가 요구하는 이터레이터의 조건은, ++멤버연산자가 정의되어 있고, *(역참조)연산자가 정의되어 있으며, 세번째 인자로 주어지는 함수객체의 첫번째 인자의 타입과 *(역참조)연산자가 리턴하는 타입이 같으면 그것으로 만족이다. 이미 알고 있는 바와 같이, 포인터타입에는 ++연산자와 *연산자가 built-in 연산으로 정의되어 있으며, print_int 함수의 인자 타입 int와 *연산자의 리턴타입이 일치하므로 아무 문제 없이 for_each의 인자로 쓰일 수 있다.

물론 for_each는 C스타일의 배열보다는 STL컨테이너와 함께 쓰일때 더욱 유용하다. 만약 지금까지 아무 생각없이 C++로 for, 혹은 wile루프를 작성하고 있었다면 for_each알고리듬으로 바꿔보는 것은 어떨까. 물론 for_each를 쓰는 것이 어색하고 직관적이지 않을 때도 있다. 그러나 STL 알고리듬을 쓰는 연습을 하면 객체지향적인 사고를 기르는데 도움이 된다. 이미 밝혔듯이, for_each를 비롯한 많은 알고리듬들이 수행할 작업을 지정하기 위해 함수객체를 사용한다. 함수객체는 ()연산자, 즉 function call operator를 오버로딩한 클래스의 객체로서, 함수호출을 캡슐화한 객체를 말한다. 즉, STL알고리듬을 사용하는 연습을 통해 작은것부터 객체로 생각하는 습관을 기르는데 도움이 된다고나 할까, 그런 잇점이 있을 수 있다.

물론, 그보다 더 큰 잇점은 깔끔하고 안정적인 코드를 얻을 수 있다는 것이다. 우리가 아무리 루프를 주의깊게 작성해도 범위를 벗어난다던가, 아니면 무한루프로 빠지는 버그를 만들지 않는다고 장담할 수는 없다. 그러나 STL알고리듬은 이미 검증된 코드이므로 우리가 직접 만드는 루프보다 그러한 문제는 확실히 적어질 것을 기대할 수 있다.
Posted by uhm