Sometimes when we distribute our program compiled by Visual Studio C++ 2010, the executables complain about the missing file, 'msvcp100d.dll'.
It is because of the standard C++ library MSVCPRTD.lib which is linked to your program. Basically, it imports the DLL 'msvcp100d.dll'. So, when the file is missing, it complains.
The most simple remedy for this problem is changing the property of your project.
Go to your project properties -> configuration attributes -> C/C++ -> code generation -> runtime library. Then, change the /MDd option to /MTd. (If you're compiling in release mode, you should change /MD to /MT). /MDd is default value for almost every Visual C++ Project, especially in Debug mode. Following is the screen shot of the project properties page.
Actually I'm using Korean version of Visual C++. So if you are using English version of Visual C++, above screen shot might not help you much. But I think the option tree shown above MUST have the same structure, so if you can follow the tree, I think I will be fine. :-)
간혹가다 메모리 관련 문제를 만났는데 쓸만한 디버깅 툴이 없을 때가 있다. 가령 valgrind 같은 툴은 최신 버전의 Mac OS X에서는 컴파일이 되질 않는다. 이런 문제를 만났을 때에는 해결 방법이 묘연하다. 동적으로 할당된 메모리가 중복 해제되지는 않는지 정도만 알 수 있으면 좋겠는데, 그런 문제도 gdb로는 추적하기가 좀 난감하다.
그런 경우, 다음과 같은 매크로를 만들어 두면 도움이 될 수도 있다. debug.h 코드를 보자.
printf "unallocated memory is freed by %s %s %s \n",
array[1], array[2], array[3]
elsif entry != nil
len = entry.mark_dealloc( alloc_info )
if len > 1
printf "MULTIPLE DEALLOCATION===================="
entry.deallocer.each do |d|
printf "%s %s %s", array[1], array[2], array[3]
end
printf "MULTIPLE DEALLOCATION--------------------"
end
end
end
end
alloc_list.each do |e|
unless e.is_deallocated?
printf "not deallocated: %s %s %s\n", e.file, e.line, e.address
end
end
end
이렇게 하면 할당되었는 데 해제되지 않은 메모리나, 두 번 이상 해제된 메모리에 관한 정보를 화면에 찍어준다. 쓸만한 메모리 디버깅 툴이 없을 때 써 볼 만 하다.
Emacs하고는 좀 달라서 vi 편집기와 쌈빡하게 연동되는 우아한 방법은 아직 없는 상태입니다. (그나마 vimgdb가 제일 가깝습니다. 설치후 한번 돌려보고 나중에 다시 글을 쓰도록 해 보죠. ^^)
하지만 굳이 vi와 연동해야 할 필요가 없다면, cgdb를 쓰는 것이 낫습니다. yum install cgdb 하면 설치가 가능합니다. 굉장히 경량이라 설치에 드는 시간도 거의 0입니다. vi와 연동되는 형태의 인터페이스는 아닙니다만, 적어도 gdb를 실행시키면서 소스 코드를 함께 볼 수 있다는 점은 편리합니다. 사용법도 극도로 간단합니다.
보시다시피 창이 위 아래로 나뉘어져, 아래쪽에서는 gdb 명령들을 입력할 수 있고 위 창에서는 소스 코드를 브라우징 할 수 있습니다. curses 라이브러리 기반이고, Linux에서 사용할 수 있습니다. cgdb를 띄워서 디버깅을 하고, 한쪽에서는 다른 창을 열어 vi로 소스 코드 편집을 하면 되겠습니다 ^^;;
제가 애용하는 에디터는 vi입니다. 모든 기능을 다 알고 있지 못한데도, 주구장창 이 에디터만 사용하고 있습니다. Emacs를 배워보려고 시도했던 적이 있습니다만, 단 한번도 성공한 적이 없습니다. (Emacs를 자유자재로 쓰시는 분들께 경배를...)
하지만 그렇다고 vi가 저열한 에디터인건 아닙니다. 사실 vi와 Emacs 에디터 중 어느 것이 더 낫느냐를 두고 많은 프로그래머들이 "종교전쟁"에 가까운 입씨름을 해 왔습니다만, 아직도 결판은 나지 않았습니다. (저는 vi를 선호함에도 Emacs가 더 강력하다고 느낍니다. 개인적으로는, '선호도'를 두고 입씨름 하는 것이 세상에서 가장 미련한 짓이라고 생각합니다.)
말이 또 샛길로 샜는데 -_-; 아무튼 vi와 ctags 유틸리티를 함께 사용하면 굉장히 빠른 소스 코드 브라우징을 할 수 있습니다. 간단히 그 사용법을 알아보겠습니다.
1. 첫 번째 단계 : source code에 태그를 단다
ctags를 사용해 브라우징할 대상이 되는 소스 코드들을 입력으로 주어, tags 파일을 생성합니다. 다음과 같이 합니다.
%> ctags *.{h,cpp}
이렇게 하면 같은 디렉터리 아래에 tags 파일이 만들어집니다.
2. 두 번째 단계 : source code 브라우징
이제 소스 코드 브라우징을 해 봅니다. vi를 시작할 때 하는 방법이 있고, vi 안에서 하는 방법이 있습니다.
a. vi 기동시 특정한 함수 코드의 시작 부분으로 점프하는 방법
%> vi -t function_foo
위와 같이 하면 function_foo의 코드가 있는 부분부터 볼 수 있습니다.
b. vi 안에서 특정한 함수의 시작 부분으로 뛰는 법
vi의 : 모드에서 다음과 같이 입력합니다.
:tag function_name
그러면 vi 커서가 function_name 함수의 시작 부분으로 뜁니다. 소스 코드 위의 특정 위치에 커서를 가져다 놓고 Ctrl + ]를 누르는 방법도 있습니다. 그러면 커서가 위치한 곳에 놓인 함수 이름과 같은 이름을 갖는 함수 코드 시작 부분으로 커서가 점프합니다.
이전 위치로 도로 돌아가려면 Ctrl + t를 누르면 됩니다. 계속 누르면 원래 위치로 돌아갈 수 있습니다. (history가 유지됨)
그런데 ctags의 한가지 안좋은 점이라고 한다면, C++에서는 같은 이름을 갖는 함수가 엄청나게 많이 정의될 수가 있는데, 그럴 경우 화면에 같은 이름을 갖는 함수들의 번호가 떠서 그 중 어디로 점프할 것이냐를 묻는다는 점입니다. 하긴 ctags 프로그램이 컴파일러도 아니니, 그런 사항까지 정확하게 처리하기는 좀 힘들겠어요.
const list<MyClass>& find_myclass_list( ... ) { // 구현은 여기에 }
그런데 find_myclass_list 함수를 구현하다보면, 내가 찾아서 반환해야 할 list<MyClass>가 없는 경우가 있어요. 그런 경우라면 기존에 없던 객체에 대한 reference를 반환해야 한다는 부담에 직면하게 됩니다. 그래서 보통 이런 경우를 이런 식으로 해결하려고들 합니다. 우선 가장 단순한 첫번째 방법은, find_myclass_list의 반환값 타입을 reference에서 pointer로 바꾸는 겁니다.
const list<MyClass>* find_myclass_list( ... ) { // 구현은 여기에 }
이렇게 하면 반환할 리스트가 존재하는 경우에는 그 리스트에 대한 포인터를 반환하면 되고, 없으면 0 값을 반환해주면 됩니다. 그러면 받는 쪽에서 그에 대한 처리를 해주면 되겠죠. 하지만 이 방법은 몇가지 단점을 가집니다. 첫 번째는 이미 인터페이스가 확정되어서 코딩이 진행중일때에는 저런 변경 작업을 하기가 어려울 수 있다는 것이고, 두 번째는 이런 식으로 코딩을 할 경우, 저 인터페이스를 이용하는 쪽에서 반환값이 0(NULL)인지 아닌지를 명시적으로 검사하는 코드를 작성해야만 한다는 것이 바로 그것입니다.
따라서 가급적이면 인터페이스를 변경하지 아니하고 아주 적은 양의 코딩을 통해서 문제를 해결할 수 있다면 좋을 겁니다. 가장 좋은 방법은 empty list를 반환하는 것이죠. 왜 그럴까요? empty list를 반환하면 받는 쪽에서는 리스트에 대한 iteration logic만 작성하면 되거든요. 리스트 안에 아무것도 없다면 어차피 iteration해봐도 안에 아무것도 없으니, 아무 동작도 하지 않게 되겠죠. 그럼 아래와 같이 하면 될까요?
아마 C/C++의 메모리 관련 문제에 익숙하신 분들이라면 잘 아시겠지만, local automatic 변수에 대한 reference를 반환하는 것은 자살행위입니다. 저 메모리는 함수가 리턴되는 순간 사라질 운명의 변수에 할당되어 있는 메모리고, 함수 반환결과로 받은 주소값을 통해 저 메모리를 엑세스하는 순간 프로그램은 뻗어버리게 될 테니까요. 이미 사라진 변수에 대한 참조는 segmentation fault나 general protection fault같은 험악한 오류를 유발하게 될 겁니다.
그럼 아래와 같이 하면 됩니까?
const list<MyClass>& find_myclass_list( ... ) { ... list<MyClass>* r = new list<MyClass>(); return *r; }
얼핏보면 우아한 해결책인것 같아 보이기도 합니다만, 그러면 저 메모리는 대체 누가 반환합니까? 저런 함수를 빈번하게 사용할 경우, 결국 memory leak이 발생하게 되겠죠.
그래서 제가 고안해 낸 해결책은 template을 사용해서 아래와 같은 클래스를 설계하는 것이었습니다.
위의 구현에서 static function Tlist를 사용한 이유는, template을 사용할 경우 static 변수 정의와 이용에 약간의 제약이 존재하기 때문이며, 이를 가장 간단한 방법으로 피해가기 위해서입니다. 조금만 살펴보시면 위의 template이 어떠한 원리로 동작하게 되는지 짐작하실 수 있으리라고 생각되므로 자세한 설명은 피하겠습니다.
흔히 동적으로 많은 양의 메모리 할당을 하고자 할 때 new를 사용해서 배열 형태로 메모리를 잡는 경우가 많죠. int 나 char 같은 간단한 타입의 메모리면 이런 식으로 할당해서 사용하는데 아무우런 문제가 없는 것이 당연하겠습니다만. 클래스로 정의된 타입을 사용해서 배열 메모리를 할당하려면 생각해봐야 할 점이 생각외로 많습니다.
string* p = new string[1024];
위와 같이 해서 메모리를 잡는 경우를 생각해 보죠. string에는 기본 생성자(default constructor)가 정의되어 있으니까 괜찮습니다만, 사실 이 방법은 "기본 생성자가 정의되어 있지 않은 타입"에는 적용할 수 없는 방법입니다. 심심하시면 기본 생성자를 정의하지 않고 다른 생성자만 정의해 둔 사용자 정의 타입에 대해서 배열로 메모리를 잡아 보세요. 컴파일 시 에러납니다. -_-
그렇다면 기본 생성자를 반드시 정의해 주어야 하느냐... 하는 고민에 빠지게 될텐데요. 물론 여러분이 만들어 놓은 타입의 경우에는 "기본 생성자가 정의되어 있지 않아서 생긴 문제를 푸는 가장 간단한 방법은 기본 생성자를 정의하는 것이다"라는 원칙에 입각하여, 기본 생성자 코드를 추가해 문제를 해결할 수 있습니다. 그런데, 기본 생성자를 추가하는 것이 아무리 봐도 불합리한 클래스를 사용하고 있는 경우나, 남이 만들어 놓은 클래스를 쓰는 경우에는, 기본 생성자를 추가할 수 없으므로, "배열을 선언할 수 없다"는 문제에 봉착하게 됩니다. 이런 문제를 둘러가는 가장 간단해 보이는 방법은 아마 아래와 같이 코드를 작성하는 것일 겁니다.
하지만 한가지 주의할 것은, 이렇게만 하면 "메모리는 잡혔으나 객체는 생성되지 않는다"는 겁니다. 위의 코드는 메모리 청크를 잡아서 그에 대한 포인터를 그냥 p에 집어 넣은 것에 불과해요. 그러니 객체 초기화에 대한 부분을 어떻게 든 해 주어야 합니다. 어떻게 하는 것이 좋을까요? 네. 답을 아시는 분들도 있겠습니다만, 'placement new'를 사용하면 됩니다.
for ( unsigned int i = 0; i < 1024; ++i ) { new ( &p[i] ) string ( "" ); }
위의 코드는 무슨 뜻입니까? &p[i]가 가리키는 메모리에 string 타입의 객체를 잡으라는 뜻이죠. placement new 를 쓰면, 객체의 초기화는 이루어지지만 메모리는 잡지 않습니다. new 연산자에 인자로 전달된 주소에 메모리가 잡혔다고 가정하게 되죠.
자. 그럼 이제 모든 문제가 다 해결된 건가요? 일단 이번 글에서 하고자 한 이야기는 여기까지 해서 다 전달한 것 같습니다. 그런데, 여기까지만 하고 잊어버리게 되면 또다른 문제가 생길 수 있어요. 메모리 반환에 대한 문제죠. delete [] p 하면 되나요?
통상적으로는 delete[]를 호출하면 p 가 가리키는 배열 내의 모든 원소에 대해서 소멸자가 호출된 다음에 메모리가 반환되죠. 될거 같기도 하죠? 그런데 이상한 문제가 생길 가능성이 생기게 됩니다. 메모리를 할당할 때 ::operator new를 썼기 때문에, 이 메모리는 1바이트 짜리 배열이지 sizeof(string) 크기 원소를 갖는 배열이 아니에요. 그러니 delete[] p와 같이 p의 타입에 종속적인 반환 연산을 하게 되면 문제가 생길 수 있게 됩니다. 두 연산 간에 bookkeeping 정보가 다를 수 있거든요. 그러니까 반환 할 때도 어쩔 수 없이 ::operator delete ( p )를 호출해야 합니다.
그런데 ::operator delete는 메모리만 반환하지 소멸자는 불러주지 않거든요. 여기에 또 문제가 있죠. 그러니까 ::operator delete를 쓸려면 소멸자를 일일이 명시적으로 불러줘야 하죠.
for ( unsigned int i = 0; i < 1024; ++i ) { p[i].~string(); } ::operator delete( p );
그러니 이런 스타일의 코딩은 가급적 하지 않는 것이 좋겠습니다... 만 STL을 구현하신 분들은 이런 테크닉을 꽤 많이들 쓰셨다고 합니다. STL같은 거대 라이브러리를 설계하고 구현하는데 일반적인 C++ 코딩 가이드라인만 지켜서는 어렵겠죠. 아무튼, 이런 코딩을 할 일이 많이 생기지 않기를 바라는 수 밖에요. X-)
오늘 같이 일하는 분들 중 가운데 한분으로부터 "왜 제 프로그램을 실행시키면 destructor가 2번 불리는 거죠?"라는 질문을 들었습니다.
소멸자는 명시적으로 불러주지 않는 한 (뭐 가령 obj.TheClass::~TheClass()와 같이 해 주지 않는 한) 절대로 두 번 불리지 않습니다. 따라서 '소멸자가 두 번 호출된다'는 증상은, 실제로는 '어디선가 객체가 두 개 생성되고 있기' 때문에 발생합니다.
따라서 그분과 저는 어디서 객체가 두 번 생성되는지를 찾아보기 시작했습니다. 그 분이"객체를 두 번 생성한 적이 없는데요?"라고 고개를 갸웃거리기 시작하셨기 때문에, 저는 문제는 딱 한가지 뿐일 거라고 생각했습니다. 여기서 '두번 생성'되는 오류를 겪고 있는 객체의 클래스를 B라고 하고, 그 클래스를 사용하는 다른 클래스를 A라고 해 보겠습니다. 저는 그 분께 "혹시 다음과 같이 작성된 코드가 있나요"라고 물었습니다.
class A { B b_obj;
public :
...
const B get_b_obj() const { return b_obj; } };
여기까지 이야기가 되고 나니까, 그분 혼자서도 버그를 찾아낼 수 있었습니다. 이 분이 작성한 코드의 문제는, 함수의 반환값을 선언하는 부분에 실수로 &를 빼먹는 바람에 get_b_obj()가 호출되는 순간 b_obj의 복사본 객체가 만들어졌다는 점이었습니다. 객체가 두개가 되었으니, 소멸자가 두 번 불리는 것처럼 보였을 밖에요.
const B& get_b_obj() const { return b_obj; }
그럼 이런 문제를 방지하려면 어떻게 해야 하나요? 다음과 같은 대비책들을 생각해 볼 만 합니다.
B의 복사 생성자를 private으로 선언한다.
A의 get_b_obj() 메소드의 반환값을 const B* 로 만든다
만일 질문하신 분이 B의 복사 생성자를 private로 만들어 두었더라면 위의 오류를 컴파일 시간에 잡을 수 있었을 겁니다. 복사 생성자가 private이니, 반환값의 type으로 const B&를 해야 할 것을 const B와 같이 했으면 오류 메시지가 떴겠죠. 그러니 '여러 개의 객체를 만들 일이 없는' 클래스에 대해서는 복사 생성자를 private로 해두는 것도 좋겠어요.
또다른 해결책으로는 객체 레퍼런스 대신 포인터를 반환하는 것을 생각해 볼 수 있습니다. 포인터를 반환하도록 하면 저런 오류는 상당부분 줄일 수 있습니다. const B*에서 '*'를 빼먹는 실수를 저지르면 똑같은 문제가 발생하지 않느냐고 생각하실 수도 있는데, 그러려면 return &b_obj; 에서 &를 빼먹는 실수도 저질러야 하기 때문에 사실상 확률은 굉장히 낮아지죠. (*와 &를 빼먹는 실수를 한꺼번에 저질러야 저런 문제가 발생하거든요). 물론 포인터를 별로 좋아하지 않는 분들께는 이런 방법이 별로 마음에 들지 않으시겠지만 말이에요.
C++ 프로그래밍을 하다 보면 간혹 이와 같은 희귀한 오류 메시지를 만나게 되는 경우가 있습니다. 보통 다음과 같은 양상으로 나타나게 되는데요.
pure virtual method called terminate called without an active exception
이런 오류 메시지가 나타나는 원인은 이렇습니다. 다음 코드를 보시죠.
class A {
public : A() {}
void init() { .... virtual_method(); .... }
virtual void virtual_method() = 0; };
class B : public A {
public : B() {}
void virtual_method() { using namespace std; cout << "this is the implementation of pure virtual function A::virtual_method()\n"; } };
어디서 많이 보던 패턴이죠? ^^ 상위 클래스에는 알고리즘의 골격만 들어가고, 하위 클래스에는 해당 알고리즘을 구성하는 부분 알고리즘(위의 경우에는 virtual_method())의 실제 구현이 들어가는 그런 구조입니다. 알고리즘의 전체 골격은 바뀌지 않으면서, 그 세부사항만 하위 클래스에 따라 달라질 때 유용한 구조입니다.
그런데 이런 식으로 하는 것 자체에는 그다지 문제가 없는데, 문제는 init()을 호출하는 위치를 A 클래스의 생성자 안으로 이동시키게 되는 순간 발생합니다.
class A {
void init() { .... virtual_method(); .... }
public : A() { init(); }
virtual void virtual_method() = 0; };
겉보기에는 꽤나 그럴싸하죠. B bb; 하는 순간 B 객체에 포함된 A 부분이 생성될 테고, A 부분은 자신의 멤버 변수 초기화를 마친 다음에 init() 함수를 불러서 알고리즘을 구동시키고, 그러면 그 알고리즘의 일부로 B 클래스에 정의된 멤버 함수가 자동적으로 호출되어 객체 전반적인 초기화를 완료하는...
그런데 이런 식의 초기화는 잘 될 턱이 없습니다. A가 private 함수 init()을 호출하는 순간 B::virtual_method가 호출되는데, 아직 객체 bb의 B 부분은 초기화가 되지도 않았거든요. -_- 그러니 B::virtual_method가 온전히 동작할 리가 없습니다. 사실 호출되는 것 자체가 불가능하죠. 객체 bb의 B 부분이 초기화되지 않았으니, 가상함수 테이블도 만들어지지 않았을 테니 말이에요.
그러니 위의 코드는 컴파일은 잘 됩니다만 (그러니 프로그래머는 '모든 순수 가상함수들을 구현했다니깐!'하는 식으로 착각하게 됩니다) 실제로 돌려보면 런타임에는 해당 가상함수의 구현이 발견되지 않습니다. 그래서 컴파일 시간이 아닌 실행 시간에
pure virtual method called terminate called without an active exception
이런 괴상망칙한 오류가 발생하게 되는거죠. 나는 분명 구현했는데 그 함수가 아직도 '순수 가상 함수라니...' 정말 짜증나는 상황이지 않나요? ㅋㅋ
Effective Java라는 책에서도 지적되었던 것 같습니다만, 이런 이유로 해서 상위 클래스의 '생성자'안에서 하위 클래스의 메소드를 호출하는 것은 불가능하거나, 가능하더라도 피해야만 합니다. 설사 저런 오류가 나지 않더라도, 초기화되지 않은 메모리에다가 뭔 짓을 하려고 하면 그게 제대로 될 리가 없거든요.
- - -
이 글을 쓰고 웹 서핑을 하다보니 Meyer 선생도 자신의 저서(Effective C++ 3rd Edition, Item9)에서 같은 문제를 지적했더군요.
Never call virtual functions during construction or destruction.
이 문제에 대한 좀 더 상세한 지식을 얻고 싶으시다면, 여기에 가보시는 것도 좋겠습니다. :-)
요즘 C++로 Admission Control System을 개발중입니다. 근데 프로그램을 짜다 보면 으례 그렇겠지만 스트링을 숫자로, 숫자는 스트링으로 변환할 일이 많이 생기더군요. 종전에는 atoll같은 함수를 써서 그런 변환작업을 하고 있었는데, 웬지 오늘은 stringstream 클래스를 써서 그걸 해보고 싶더군요. 어쩌다가 Bjarne 선생님의 이 글을 보게 되었거든요. "How do I convert an Integer to a String"이라는 것이 제목인데요. stringstream 클래스를 사용해 itos라는 함수(integer를 문자열로 바꾸는)를 구현합니다.
그래서 이번에는 문자열을 숫자로 바꾸는 작업을 stringstream을 써서 해보기로 했습니다. 거의 비슷하게 하면 될것 같더군요.
#include <iostream> #include <sstream>
using namespace std;
long long stoll(const string& v) { stringstream ss; ss << v;
long long ret; ss >> ret;
return ret; }
int main() {
long long r;
for ( int i = 0; i < 10000000; ++i ) { r = stoll("48623948209834029"); } }
네. 이렇게 구현한 코드는 아주 그럴싸하게 잘 돌아갑니다. 나름대로 'C++'적이기도 하지요. 하지만 여러 가지 이유로 저는 위의 코드를 사용하기를 포기했습니다. 왜 그랬냐고요? 위의 코드를 Unix time 명령과 함께 실행해보면, 다음과 같은 결과를 얻습니다.
[bjlee@bjlee-xnote test]$ time ./a.out
real 0m24.972s user 0m23.349s sys 0m0.060s [bjlee@bjlee-xnote test]$
하지만 같은 프로그램을 stdlib.h에 선언되어 있는 atoll을 써서 다음과 같이 하면 어떨까요?
int main() {
long long r;
for ( int i = 0; i < 10000000; ++i ) { r = atoll("48623948209834029"); } }
그랬을 경우에는 다음과 같은 결과를 얻습니다.
[bjlee@bjlee-xnote test]$ time ./a.out
real 0m2.306s user 0m2.276s sys 0m0.008s [bjlee@bjlee-xnote test]$
간단한 실험으로도 성능 차이가 꽤 크다는 것을 확인할 수 있습니다. 객체 생성과 같은 기타등등의 오버헤드가 끼어들기 때문에 stringstream쪽의 퍼포먼스가 더 안좋게 나온 것이겠죠. 그럼 Bjarne 아저씨는 int를 string쪽으로 변환하는 가장 간단한 방법으로 왜 stringstream을 추천한 것일까요? 그게 궁금해서 반대쪽도 실험을 해봤습니다.
#include <iostream> #include <sstream>
using namespace std;
string itos(const long long& v) { stringstream ss; ss << v;
string ret; ss >> ret;
return ret; }
int main() {
string p;
for ( int i = 0; i < 10000000; ++i ) { p = itos(324879829); } }
위의 코드의 경우 time으로 시간을 재 보면 결과는 다음과 같습니다.
[bjlee@bjlee-xnote test]$ time ./a.out
real 0m19.790s user 0m19.677s sys 0m0.012s [bjlee@bjlee-xnote test]$
for ( int i = 0; i < 10000000; ++i ) { p = itos(324879829); } }
이 경우의 성능을 재 보면 다음과 같습니다.
[bjlee@bjlee-xnote test]$ time ./a.out
real 0m7.683s user 0m7.104s sys 0m0.032s [bjlee@bjlee-xnote test]$
어쨌던 stringstream을 쓰게 되면 성능은 배 이상 떨어지는군요.
그렇다면 Bjarne 아저씨가 int를 string으로 변환할 때 stringstream을 쓰는게 가장 간단하다고 추천한 이유는 무엇일까요? 이유도 간단합니다. string을 int로 변환하는 쪽은 그럴듯한 솔루션이 이미 많이 있습니다. (atoll이나 atoi와 같은) 거기다 이런 함수들은 '더 이상 간단해질 수 없을 정도로' 단순한 함수들이어서, 그 함수가 '그다지 우아해보이지 않는다는 이유로' C++적인 해결책을 고안해 낸다는 것이 말이 되질 않아요. 방금 위에서 실험한 결과로도 증명되는 이야기입니다만, 성능도 더 형편없어질 가능성이 높죠.
반면 int를 string으로 변환하는 방법을 찾아보면, 그다지 간단한 것이 없습니다. 제가 선호하는 방법은 (위의 예제 코드에서도 써먹었습니다만) sprintf를 쓰는 것인데, 그것도 버퍼를 잡는 등의 사전 작업이 필요하니까 그렇게 편하지는 않아요. 그러니 그런 경우에는 'stringstream'을 쓰는게 나을 수도 있겠죠. Bjarne의 말처럼, '간단함'이라는 견지에서 보면 말이에요. 거기다 stringstream 클래스 안에는 오만가지 데이터타입을 다 처리할 수 있도록 오버로딩된 연산자들이 잔뜩 구현되어 있습니다. 성능은 안좋을지 몰라도, "%lld같은 옵션을 전부 기억하고 있지 않아도 구현을 할 수 있으니까, 편하다는 것이죠. (거기다 template을 섞어쓰면 아마 더 편해질겁니다 ㅋㅋ)
그렇다면 오늘의 결론은...
int -> string 변환이 필요한 경우에는 stringstream을 쓰는 것이 '간단하'다.
반대의 변환이 필요한 경우에는 stringstream을 써 봐야 코드가 간단해 지지 않는다
오래된 글이라서 답글을 올려도 되려나 하고 생각합니다만,
int(혹은 long) -> string으로 하는 것은 standard가 없습니다.
vendor에 따라서 implementation이 제 각각이기 때문입니다.
그렇기 때문에 Dr. Strousrub은 stringstream을 추천하는 것입니다.
제 소견으로는 'portable'이라는 것이라고 생각합니다.
댓글을 달아 주세요