C++ 이야기 열일곱번째: 임시 객체는 임시 객체일 뿐
[이글의 최신 Update 문서는 항상 여기에서 확인할 수 있습니다]
오랜만에 C++ 이야기를 포스팅합니다. 제가 그 동안 상당히 외도를 많이 했었죠 ? 원래 블로그를 시작할 때만 해도 소프트웨어 개발 이야기를 전문적으로 다루려고 했는데, 제가 워낙 다방면에 관심이 많다 보니 여기 저기 찌르고 다니느라 이제서야 제 전문 분야로 돌아오게 됐습니다. ^^ 제 블로그에서 소프트웨어 개발 이야기와 관련된 글을 기대하시는 독자분들께는 정말 죄송하지만 제 성격상 어쩔 수가 없더군요.
이번 글에서는 C++에서 임시 객체와 관련된 재밌는 문제를 다뤄볼까 합니다. 자~ 우선 다음 코드를 한 번 보시죠.
void f(X a1, X a2)
{
extern void g(const X&);
X z;
// . . .
z = a1 + a2;
g(a1 + a2);
// . . .
}
위 코드에서 임시 객체가 쓰이는 부분을 찾으실 수 있으시겠어요 ? 예~ 두 군데가 있습니다. a1 + a2 값을 z 에 assign 하기 전과 g() 함수의 인자로 넘겨줄 때 임시 객체가 사용됩니다. 이 임시 객체라는 녀석을 곰곰히 들여다 보고 있으면 자연스럽게 떠오르는 질문이 하나 있습니다.
"이 녀석은 언제 destructor 가 불리지 ?"
궁금증을 해결하려면 직접 부딪혀 보는 것보다 좋은 방법은 없을 것입니다. 다음과 같은 간단한 프로그램을 실행시켜 보기로 하겠습니다.
// X.h
#ifndef X_H
#define X_H
class X
{
private:
int _i;
public:
X(int i = 0) : _i(i) {}
~X(void);
X operator + (const X& x1);
int get();
};
#endif
// X.cpp
#include "X.h"
#include <iostream>
using namespace std;
X::~X()
{
cout << "~X() called" << endl;
}
X X::operator +(const X &x1)
{
return X(_i + x1._i);
}
int X::get()
{
return _i;
}
// main.cpp
#include "X.h"
#include <iostream>
using namespace std;
int
main(void)
{
X x1(10);
X x2(20);
X z;
z = x1 + x2;
cout << "The first use of temporary object" << endl;
cout << "x1 + x2 = " << (x1 + x2).get() << endl;
cout << "The second use of temporary object" << endl;
cout << endl << endl << "Exitting..." << endl;
}
이걸 컴파일&빌드한 후 실행해보면 다음과 같이 출력되는 걸 확인하실 수 있을 것입니다.
~X() called
The first use of temporary object
x1 + x2 = 30
~X() called
The second use of temporary object
Exitting...
~X() called
~X() called
~X() called
자~ 이제는 대충 감이 잡히시죠 ? 위 결과를 봐서는 아무래도 임시 객체가 사용된 statement의 끝(end of statement; EOS)에서 destructor가 호출되는 것 같습니다. 임시 객체의 lifetime 은 임시객체가 사용된 statement 의 끝까지라고 보면 될 것 같습니다.
그럼 다음과 같은 코드는 어떻게 될까요 ? operator const char*() 의 구현이 따로 복사본을 리턴하는 것이 아니라 내부 char 배열에 대한 pointer 만 넘겨 주도록 구현되어 있다고 가정해 보시기 바랍니다(보통은 이런식으로 optimize 를 해서 구현하기 마련이죠).
class String {
// ...
public:
friend String operator+(const String&, const String&);
// ...
operator const char*(); // conversion operator to C-style string
};
void f(String s1, String s2)
{
const char* p = s1+s2; // 바로 이 부분이 문제!!
printf("%s", p);
// ...
}
언뜻 보기에는 위 코드는 아무런 문제가 없어 보이지만 임시 객체를 사용하는 부분에서 포인터 p 는 s1+s2의 결과를 담고 있는 임시 객체 내부 char 배열을 가리키게 됩니다. 그리고나서 printf()의 인자로 p 가 넘어가기 전에 s1+s2의 결과를 담고 있던 임시 객체가 destroy 됩니다. 즉, p는 더이상 valid 한 주소를 가리키지 않는 것이지요. f() 함수 코드를 다음과 같이 고치면 아무런 문제가 없을 것입니다.
void f(String s1, String s2)
{
printf("%s", (const char*)(s1+s2));
// ...
}
조금은 당황스럽긴 하시겠지만 이것이 C++의 규칙이라는군요. 그러니 임시 객체를 쓰실 때는 각별히 주의하셔야겠습니다. 다음과 같이 기억하고 계시면 위와 같은 실수는 막을 수 있지 않을까 합니다. ^^
"임시 객체는 임시 객체일 뿐 계속 쓰지 말자"
그런데, 이런 걸 보면 왜 언어를 이런식으로 정의해 놓았을까라는 생각이 들지 않으세요 ? 임시 객체의 lifetime이 좀 더 오랫동안 예를 들어, block 의 끝까지 유지되도록 하면 더 좋지 않을까요 ? 이 문제에 대해 한 번 나름대로 고민해 보시면 재밌는 문제들을 발견할 수 있을 것입니다. ^^
블로그를 구독하는 방법을 잘 모르시는 분은 2. RSS 활용을 클릭하세요.
RSS에 대해 잘 모르시는 분은 1. RSS란 무엇인가를 클릭하세요.
다음글 예고편: 바람직한 소스 코드 관리 전략 #2
다음글도 기대해 주세요~~~ :)