C++ 이야기 열아홉번째: 임시 객체가 충분히 임시스럽기 위하여

Posted at 2008. 3. 31. 06:18 // in S/W개발/C++ 이야기 // by 김윤수


[이글의 최신 Update 문서는 항상 여기에서 확인할 수 있습니다]

2008/02/24 - [S/W개발/C++ 이야기] - C++ 이야기 열일곱번째: 임시 객체는 임시 객체일 뿐

지난글에 이어 임시 객체에 대한 이야기를 풀어볼까 합니다. 지난글 마지막에 제가 던졌던 질문에 대한 답변이 될 수 있을 거라 생각합니다.

그런데, 이런 걸 보면 왜 언어를 이런식으로 정의해 놓았을까라는 생각이 들지 않으세요 ? 임시 객체의 lifetime이 좀 더 오랫동안 예를 들어, block 의 끝까지 유지되도록 하면 더 좋지 않을까요 ?

본격적으로 이야기를 시작하기 전에 시감상부터 하시죠. 웃자구요 ^^

C++ 세상 태초에 임시 객체가 있었으니
잠깐 사용될 운명을 거부하며
메모리 세상으로 도망가 셀들을 닥치는 대로 먹어치우더라

창조자가 메모리 세상으로 화해의 천사를 보냈으나
대화를 거부하고
혼돈의 컴파일러 세상에 이르러
코더의 삶을 더욱 고달프게 하니

인내의 한계에 다다른 창조자와
그를 돕는 천사장들이
임시 객체를 가둘 정교한 EOFE 감옥을 만들어
오랜 추적 끝에 그를 가두니
메모리 세상과 컴파일러 세상에
평화가 깃들더라

잡히지 않은 임시 개체의 추종자들
때로 그의 탈옥을 감행하였으나
코더의 피나는 노력으로
버그 없는 코드 세상이 되니라

오늘은 이 시를 풀이하면서 임시 객체가 유효한 범위가 왜 지금처럼 정해졌는지를 말씀드리겠습니다.

초기 임시 객체 lifetime 의 문제점

C++ 세상 태초에 임시 객체가 있었으니
잠깐 사용될 운명을 거부하며
메모리 세상으로 도망가 셀들을 닥치는 대로 먹어치우더라


C++ 언어 창조자인 Bjarne Stroustrup이 처음 정했던 임시 객체의 lifetime 은 다른 지역 변수와 마찬가지로 block 이 끝날 때까지였다고 합니다. 이렇게 했더니 두 가지 문제가 있었다고 하는군요.

1. 이 정도로는 임시 객체의 lifetime이 충분히 오랫동안 지속된다고 할 수 없는 경우. 예를 들어, 어떤 함수 g()에서 어떤 수식의 결과로 생성된 임시 객체에 대한 pointer 를 리턴하는 경우가 있을 수 있을 것입니다. 코드로 표현한다면

X* g(X x1, X x2)
{
  X* pX = &(x1 + x2);
  return pX;
}

void f()
{
  X x1, x2
  X* pX = g(x1, x2);
  ...
}

2. 이 정도면 임시 객체의 lifetime이 너무 오랫동안 지속된다고 할 수 있는 경우. 예를 들어, 어떤 객체가 1000x1000 정도의 행렬이고, 어떤 함수 하나가 수행되는 동안 여러 개의 임시 객체가 할당된다면 block의 끝에 다다르기 전에 남은 메모리를 다 사용해 버릴 수도 있을 것입니다.

현실적으로 1번과 같은 경우가 문제 없이 수행되리라고 생각하는 사람들은 드물 것이므로 큰 문제가 안 될 것이구요. 차라리 2번 같은 경우가 문제가 될 것입니다. 그래서 그 당시 임시 객체가 여러개 생성되는 것을 방지하기 위해 사람들이 다음과 같이 각각 statement 를 '{', '}'로 둘러 싸는 경우가 많았다고 합니다.

void f(X a1, X a2)
{
  extern void g(const X&);
  X z;
  // ...
  { z = a1 + a2; }
  { g(a1+a2); }
  // ...
}

이렇게 되면 당연히 프로그래머들에게서 불만이 터져 나오겠지요.

위 시구절은 초기 임시 객체 lifetime 규칙 때문에(잠깐 사용될 운명을 거부하며) 2번 문제가 발생한 것(메모리 세상으로 도망가 셀들을 닥치는 대로 먹어치우더라)을 시적으로 표현해 본 것입니다.

개정된 규칙때문에 더욱 혼돈으로

창조자가 메모리 세상으로 화해의 천사를 보냈으나
대화를 거부하고
혼돈의 컴파일러 세상에 이르러
코더의 삶을 더욱 고달프게 하니


C++ 사용자 커뮤니티로부터 불만을 들은 Bjarne Stroupstrup은 ARM(Annotated C++ Reference Manual) 에서 임시 객체 lifetime 규칙을 완화시켜 임시 객체가 처음 사용된 시점부터 block의 끝에 이를 때 어느 때나 destroy할 수 있도록 했습니다.

그랬더니 의도와는 다르게 여러 컴파일러들이 서로 다른 정책에 취함에 따라 portable 한 코드를 짜기가 더 힘들어졌고, 결국 임시 객체가 사용된 후 바로 destroy 되는 것을 가정하고 프로그래밍할 수밖에 없어졌습니다. 그랬더니 이전글에서도 언급됐던-상당히 자주 사용됐던 C++ 이디엄-코드가 제대로 작동하지 않게 됐습니다.

class String {
private:
  char * rep_;
  // ...
public:
  friend String operator+(const String&, const String&);
  // ...
  operator const char*();
    // conversion operator to C-style string
};

void f(String s1, String s2)
{
  printf("%s", (const char*)(s1+s2));
  // ...
}


지난글에서와 마찬가지로 operator const char* ()가 String의 내부 char 배열(rep_)을 리턴하는 걸로하면 s1+s2의 결과를 담은 임시 객체에 대해 operator const char* () 가 호출된(이 시점이 임시 객체의 첫 사용 시점이 됩니다) 직후에 임시 객체가 destroy되고 printf() 로는 destroy 된 객체의 내부 char 배열(rep_)이 전달되게 되는 것이지요.

결국 문제를 해결하려다가 더 복잡한 문제를 만들어 버린 것이었습니다.

위 시구절은 Bjarne Stroustrup이 ARM에서와 같은 규칙을 제정했으나(창조자가 메모리 세상으로 화해의 천사를 보냈으나), 컴파일러들이 각기 나름의 정책을 취함에 따라(대화를 거부하고 혼돈의 컴파일러 세상에 이르러) portable 한 코드를 짜기가 힘들어 지고 그 전에 작동하던 코드도 작동하지 않게 됨(코더의 삶을 더욱 고달프게 하니)을 표현해 본 것입니다.

길고 긴 C++ 표준화 과정

인내의 한계에 다다른 창조자와
그를 돕는 천사장들이
임시 객체를 가둘 정교한 EOFE 감옥을 만들어
오랜 추적 끝에 그를 가두니
메모리 세상과 컴파일러 세상에
평화가 깃들더라


결국 이러한 문제점은 1991년부터 1993년까지 오랜동안 C++ 표준화 위원회에서 논의됐으나 확실한 결론을 내리지 못하다가 1993년경 Dag Bruck이 다음과 같은 대안들을 정리하고 각 대안들에 대한 평가를 하기에 이르렀습니다.

  1. 첫 사용 직후 destroy: 위에서 언급한 문제 발생
  2. statement 끝에서 destroy
  3. 다음 branch point 에서 destroy: flow analysis 필요
  4. block 의 끝에서 destroy: 원래의 rule과 동일. memory 소비 문제
  5. function의 끝에서 destroy: 원래의 rule 보다 더 긴 lifetime, memory 소비 문제
  6. 마지막 사용 후에 destroy: garbage collector 필요
  7. 첫 사용 직후와 block 끝 사이 언제나: ARM의 rule과 동일. 별 도움이 되지 않는 rule. 실질적으로는 1번 rule을 가정해야 함

위와 같은 대안들과 그에 대한 분석을 바탕으로 C++ 표준화 위원회는 statement의 끝(EOS)에서 destroy 하는 방안에 집중하기 시작했습니다. 그런데 EOS 라는 것의 의미를 정확히 하는데 약간 애를 먹었습니다. 왜냐면 다음과 같은 코드에서 혼란이 발생했기 때문입니다.

void h(String s1, String s2)
{
  const char* p;

  if (p = s1+s2) {
    // ...
  }
}

임시 객체를 if 의 조건이 완료됐을 때 destroy 해야할까요 아니면 if 문이 완전히 끝난 다음에 완료해야할까요 ? 표준화 위원회의 대답은 조건이 완료됐을 때 destroy 하는 것이었습니다.
다음과 같은 코드는 작동하는 것을 보장하면서

if (p = s1+s2) printf("%s", p);

다음과 같은 코드가 작동하는 것을 보장하지 못한다는게 말이 안된다고 판단했기 때문입니다.

p = s1+s2;
printf("%s", p);


그렇다면 다음과 같이 한 expression 안에서 branching 이 일어날 경우에는 어떻게 해야할까요 ?

if ((p = s1+s2) && p[0]) {
  // ...
}


p = s1+s2 가 완료된 후에 destroy 해야할까요 ? 그렇다면 p[0] 이 제대로 작동할 수 없을 것입니다. 이렇게 다양한 코드 사례들을 검토한 후, 결국 EOS의 의미는 end of full expression(EOFE) 을 뜻하는 것으로 확정되었다고 합니다. end of full expression 이란 다른 expression의 subexpression 이 아닌 expression을 뜻하는 것입니다.

이렇게 임시 객체의 lifetime을 정했더니 상당히 깔금하게 rule을 표현할 수 있고, 설명하기도 쉽고, 임시 객체가 너무 오랫동안 유지될 때의 문제점도 풀게 되는 등 상당히 여러 가지 문제점들 간의 균형을 잘 맞춘 해결책으로 평가되고 있습니다. 표준화된 이후로 컴파일러도 속속 새로운 규칙을 따르기 시작하게 됐습니다.

위 시구절은 C++ 표준화 위원회(창조자와 그를 돕는 천사장들이)에서 오랜 논의 끝에(오랜 추적 끝에) EOFE 라는 규칙을 만들어(임시 객체를 가둘 EOFE 감옥을 만들어) 표준안에 반영하였고(가두고) 이후 이 규칙의 장점으로 인해 여러 문제도 사라지고 컴파일러들이 규칙을 따르기 시작한 것을(메모리 세상과 컴파일러 세상에 평화가 깃들더라) 시적으로 표현한 것입니다.

C++ 개발자들의 주의가 필요

잡히지 않은 임시 객체의 추종자들
때로 그의 탈옥을 감행하였으나
코더의 피나는 노력으로
버그 없는 코드 세상이 되니라


물론 EOFE 규칙이 완벽하게 모든 문제를 해결한 건 아니므로 개발자들이 임시 객체로 인한 버그가 발생하지 않도록 노력해야 할 것입니다. 종종 C++ 가 표준화되기 이전에 학습했던 분들(잡히지 않은 임시 객체의 추종자들)이 그 때의 기억만 가지고 프로그래밍하면서 문제가 있는 코드를 생성할 수 있는데(때로 그의 탈옥을 감행하였으나), 새로운 표준안을 학습하여 문제가 발생하지 않도록 노력한다면(코더의 피나는 노력으로) 버그가 없고 품질이 좋은 소프트웨어를 만들 수 있으리라 생각합니다(버그 없는 코드 세상이 되니라).

다음 글을 기억하시면 임시 객체와 관련된 버그를 피하실 수 있으리라 생각합니다.

"임시 객체는 전체 Expression의 끝까지만 유지된다"

이 글을 읽으시면서 위 규칙과 더불어 임시 객체가 충분히 임시스럽기 위하여 얼마나 오래동안 여러 가지 대안들을 평가하고 분석했는지를 보시고, 여러 가지 대안을 도출하고 이를 분석적으로 접근하는데 익숙해져야겠다는 걸 느끼셨다면 제가 이 글을 쓴 진짜 의도를 파악하신 대단한 분으로 인정해 드리도록 하겠습니다. ^^

참고: The C++ Programming Language, 3rd Edition에는 다음과 같이 표현되어 있습니다.
"임시 객체는 자신이 생성된 전체 표현식의 처리가 모두 끝날 때 소멸된다"

제 글이 유익하셨다면 오른쪽 버튼을 눌러 제 블로그를 구독하세요. ->
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.

다음글 예고편: reentrant 와 thread-safe 의 차이
다음글도 기대해 주세요~~~ :)