C++ 디버깅
by null_pointer
원문 :
http://www.gamedev.net/reference/articles/article1344.asp
C++은 당신이 어떤 플랫폼을 쓰든, 당신이 디버거를 사용할 수 있든 없든 간에
디버깅에 유용한 몇가지 강력한 기능들을 갖고있다.
이 문서는 당신이 코드를 디버깅하는 데 쓸 수 있는 방법들을 나열하고,
그 방법들을 사용하기위한 환경에 대해 토의하는데 목적이 있다.
프로그래밍 언어에서 새로운 기능을 발견할 때,
사람들은 종종 그 기능의 단점(drawback)들을 무시하고 이것으로 다른 기능을 모두 대체하려고 시도하는 경향이 있다.
사실 모든 문제에 대한 완벽한 설계모델이란 존재하지 않기 때문에,
이런 경향은 소비적이고, 잘 못 설계된 코드가 되게 한다.
모든 것들은 "더 좋은" 설계모델에 맞도록 만들어져야하기 때문이다.
당신의 환경과 여러가지 다른 방법들의 상대적인 장단점을 고려하지 않고 최적의 모델을 고를 수는 없다.
Assertion, 예외(exception), 로깅(logging), 반환값(return values) 등은 모두 나름대로의 장점과 단점이 있다.
이 방법들에 대한 고찰을 나열해보도록 하겠다.
방법 0 - 아무 것도 하지 않는다.
장점: 수 많은 코드를 쉽게 쓸 수 있다. 디버그나 릴리즈에서 실행 부담이 없다.
단점: 잘 통하지 않으면 포기하는게 낫다.
이 방법은 사실 아무것도 아닌 방법이다 - 그래서 이걸 방법 0 이라고 했다.
필자는 완벽을 기하기 위해 이걸 포함시키는게 좋겠다고 생각했다.
당신이 이 방법을 자주 사용한다면, 당신의 고객들에게 양해를 구하고 전문가의 도움을 청할 것.
여기서 잠깐 필자가 생각하는 디버깅 이론과, 이 글 전반에서 사용할 몇 가지 관례들을 설명할 필요가 있을 것 같다. C++의 코드는 디버깅과 릴리즈라는 두 종류로 나뉜다.
두 종류 모두에서 코드는 기능적으로 동일해야 한다.
디버그 모드에서는 속도보다는 디버깅이 중시되고, 릴리즈 모드에서는 디버깅보다 속도가 중시된다.
물론 그 외의 다른 모드들을 설정하는 것도 가능하지만, 이 글에서는 두 가지만 사용하겠다.
디버깅은 클린업(마무리?)와는 다른 것임을 주의하기 바란다.
일반적으로 버그는 특정한 상황에서 실패하는 잘못 설계된 코드를 뜻한다.
디버깅은 그러한 버그를 찾고 제거하는 과정이다.
버그의 원인은 여러 가지가 있는데, 몇가지를 나열하자면:
- 언어, API, 다른 코드 그리고/또는 플랫폼에 대한 이해의 부족
- 나쁜 코드 디자인/구조
- 운영체제버그
- 같은 팀의 멤버들과의 잘못된 의사소통
- 급히 작성된 코드
- 도대체 아무 이유도 없는 경우 :)
코드를 작성할 때, 여러분의 코드가 완전히 독립적인 코드가 되기는 힘들다는 것을 알아야 한다.
왜냐하면, 여러분의 코드가 일반적으로 표준 라이브러리에 의존하기 때문이다.
게다가, 혼자 코드를 하는 경우는 드물기 때문에(상업적으로 성공하고자 한다면),
여러분의 코드와 팀 멤버의 코드가 상호의존적이게 되고,
(만약 라이브러리를 만든다면) 고객의 코드와도 상호의존적이게 된다.
사람들이나 소스코드의 역할을 의존관계에 관하여 묘사하는 데에 주로 사용되는 두가지 용어가 있다:
서버와 클라이언트. 당신의 코드를 사용하고 의존하는 사람들은 클라이언트라고 불린다.
당신이 그들의 코드에 의존할 때에는 당신이 그들의 클라이언트가 된다.
서버는 여기서 거의 사용되지 않지만, "코드를 작성하는 사람"이라는 뜻이다.
방법 1 - Assertions
장점 : 비교적 빠르고, 릴리즈 빌드했을 때 오버헤드가 없으며, 코딩하기가 굉장히 간단하다.
단점 : 디버그 빌드했을 때 코드를 조금 느리게 하고, 릴리즈 빌드했을 때 안전성이 없으며,
클라이언트가 디버그할 때 소스코드를 읽어야한다.
설명
assertion은 논리식으로써, 프로그램이 계속해서 제대로 실행되기 위해선 이 논리식이 참이 되어야한다. assertion 함수를 이용하고, 참이 되어야하는 논리식을 이 함수에 전해줌으로써 다음과 같이 assertion을 명시한다.
assert( this );
this가 0이면, assert 함수는 프로그램 실행을 중지하고
"assert( this )"가 소스파일의 이러이러한 라인에서 실패했다고 알리는 메시지를 출력한 후,
거기서부터 계속 실행할 수 있게한다.(and lets you go from there)
만일 this가 0이 아니면, assert는 그냥 리턴되어 프로그램은 정상적으로 계속해서 실행될 것이다.
assert 함수는 릴리즈 빌드에서 아무것도 하지 않고 오버해드를 주지도 않는다는 사실에 주의하라.
따라서 다음과 같이 쓰면 안된다.
FILE* p = 0; assert( p = fopen("myfile.txt", "r+") );
... 왜냐하면 릴리즈 버젼에서는 fopen이 호출되지 않기 때문이다! 제대로 할려면 다음과 같이 한다.
FILE* p = fopen("myfile.txt", "r+") ); assert( p );
예제
이들은 거의 항상 가정을 해야하는 새 코드를 작성할 때 쓰는 게 제일 좋다.
다음의 함수를 보자.
void sort(int* const myarray) // an overly simple example 아주 간단한 예제
{
for( unsigned int x = 0; x < sizeof(myarray)-1; x++ )
if( myarray[x] < myarray[y] ) swap(myarray[x], myarray[y]);
}
이 함수가 세운 가정들을 세어보라.
이제 디버깅하기 좀 편하게 해둔 좀 나은 버젼을 살펴보라.
void sort_array(int* const myarray)
{
assert( myarray );
assert( sizeof(myarray) > sizeof(int*) );
for( unsigned int x = 0; x < sizeof(myarray)-1; x++ )
if( myarray[x] < myarray[y] ) swap(myarray[x], myarray[y]);
}
보다시피, 괜찮아보이는 알고리즘도 다음과 같은 상황에선 통하지 않는다.
- 포인터가 null 이거나,
- array가 스택에 할당되지 않았거나 누가 배열이 아닌 단일 오브젝트의 주소를 넘겨줘서
배열 내 요소의 갯수를 알아내는데 sizeof(myarray)를 사용할 수 없는경우.
비록 이건 간단한 알고리즘이지만, 당신이 작성하거나 마주치게 될 많은 함수들은 훨씬 크고 훨씬 복잡할 것이다.
코드 한 부분이 동작하지 않게 만드는 조건이 얼마나 많은지 보면 놀라게 될 것이다.
필자가 얼마전 만지작거렸던 알파블렌딩 루틴의 일부분을 한번 살펴보자.
void blend(const video::memory& source, video::memory& destination, const float colors[3])
{
// The algorithm used is: B = A * alpha
// 사용된 알고리즘 : B = A * alpha
const unsigned int width = source.width();
const unsigned int height = source.height();
const unsigned int depth = source.depth();
const unsigned int pitch = source.pitch();
switch( depth )
{
case 15:
// ...
break;
case 16:
// ...
break;
case 24:
{
unsigned int offset = 0;
unsigned int index = 0;
for( unsigned int y = 0; y < height; y++ )
{
offset = y * pitch;
for( unsigned int x = destination.get_width(); x > 0; x-- )
{
index = (x * 3) + offset;
destination[index + 0] = source[index + 0] * colors[0];
destination[index + 1] = source[index + 1] * colors[1];
destination[index + 2] = source[index + 2] * colors[2];
}
}
} break;
case 32:
// ...
break;
}
}
이 함수가 최적화라는 이름으로 얼마나 많은 가정을 세웠는지 파악했는가? 한번 열거해보자.
assert( source.locked() and destination.locked() );
assert( source.width() == destination.width() );
assert( source.height() == destination.height() );
assert( source.depth() == destination.depth() );
assert( source.pitch() == destination.pitch() );
assert( source.depth() == 15 or source.depth() == 16 or source.depth() == 24 or source.depth() == 32 );
일반적으로, 이와같은 저레벨 코드를 최적화하면 할 수록 더 많은 가정들을 세워야한다.
필자의 함수는 소스(source)와 데스티네이션(destination) 비디오 메모리가 lock되어있어야하고
(블랜딩 함수들이 여러번 호출될 때 lock()/unlock()을 한번만 할 수 있도록),
그 둘이 다 같은 넓이, 높이, depth, pitch를 가지고 있어야한다.
이들 assertion들을 함수 첫머리에 둠으로써 나중에 다른 프로그래머가 내 함수가 왜 작동하지 않느냐,
왜 접근위반(access violation)을 일으키느냐하고 이상하게 여기는걸 방지할 수 있을 것이다.
- 모든 필요조건들이 이제 함수 첫머리에 명시되었다.
하지만, 당신은 어떤 assert 버젼을 사용하는지 조심해야한다.
만일 당신이 <cassert> 에 정의된 ANSI C assert 를 사용한다면,
당신은 assert가 만들어낸 스트링상수로 인해 아주 커다란 디버그 빌드에 얽매이게 될 것이다.
만일 이 문제를 만나게되면, 스트링상수를 만들어내는 대신 assert가 어떠한 예외를 발동하게 만들어야 한다.
또한, 모든 인수와 조건식을 체크할 필요는 없다.
- 훌륭한 프로그래머에게 있어서 어떤 것들은 너무나도 명백하다.
때로는 주석이 더 나을 수도 있는 데 그 이유는 최종 빌드의 사이즈를 증가시키지 않기 때문이다.
훌륭한 코드는 매 함수의 상단에 너무 많은 assertion 들을 필요로 하지 않는다.
만약 당신이 클래스를 작성하고 있고 assertion 을 각 멤버 함수에 넣어 상태(나 기타 다른 것)를 체크하려 한다면
아마도 클래스를 다른 클래스들로 쪼개는 것이 더 나을 것이다.
결론
여러분의 코드 전반에 걸쳐 약간의 assertion 들을 사용하는 습관을 갖는 것은 다음과 같은 장점을 갖는다:
- 여러분의 코드가 해당 환경 및 다른 코드/데이타에 대하여 취하는 가정들에 대해서 잘 염두해 두어야 한다.
그로 인해 더 나은 테크닉을 개발하고 버그를 예방할 수 있게 된다. - assertion 들을 함수의 제일 위 그리고(또는) 조건문의 바로 전후에 가져다 놓음으로써,
여러분의 코드를 사용하는 다른 프로그래머들이 더 쉽게 버그를 예방할 수 있게 해 준다. - 또한 코드의 의도, 관련된 함수/데이타에 대한 영향, 그리고 있을 지 모르는 설계 제약들에 대해서 알려준다.
- 주요 인자들을 확인하기 위해 assertion을 사용하면 버그를 추적하기 쉬워진다.
그 이유는 버그들이 눈에띄지 않는 알고리즘이나 함수안에서,
심지어 에러를 발생할 때까지 발견할 수 없는 곳에서가 아닌 인자들이 함수로 전해질때에 발견되기 때문이다. - 잘못된 리턴 값을 추적하는 것을 쉽게 한다. (DirectX 의 assert(SUCCEEDED(hr)) 를 보라) ;)
- 예외상황에 대한 설명이라든가 리턴값 에러 코드를 생각할 필요가 없다.
간단한 새로운 코드를 작성하고 빨리 테스트해보고 싶거나, assert(this) 처럼 너무나도 명백한 곳에 유용하다.
방법 2 - 예외
장점 : 자동으로 청소(cleanup)가 되고 우아하게 종료한다. 잘 다뤄주면 계속 실행될 수도 있고,
디버그와 릴리즈 빌드 모두에 통한다.
단점 : 비교적 느리다.
설명
기본적으로, 당신은 throw 키워드를 사용해서 미지의 함수호출자에게 데이터를 던지고,
스택은 누군가 데이터를 잡을(catch) 때까지 계속해서 감긴걸 풀어준다.
(프로그램이 그 자신을 역전시키는걸로 생각하라.)
당신이 예외를 잡고(catch)싶은 코드를 try 키워드로 둘러싼다.
다음을 보라.
// some function that throws an exception
// 예외를 throw하는 함수
void some_function() { throw 5; }
int main(int argc, char* argv[])
{
// just letting the compiler know we want to catch any exceptions from this code
// 이 코드에 일어나는 모든 예외를 잡기 원한다는걸 컴파일러에게 알려준다.
try
{
// if the type matches the data thrown, we execute this code
// throw된 데이터 타입과 동일하면 이 코드를 실행한다.
some_function();
} catch(const int x) {
// do something about the exception...
// 예외에 관한 뭔가의 처리를 한다...
}
}
만약 당신이 당신의 코드 안에 try 블럭을 넣지 않으면,
스택은 단순히 계속 풀려서(unwind) main 함수를 지나고, 프로그램이 종료될 것이다.
당신은 try 블럭을 모든 곳에 넣을 필요가 없다.
- 당신이 예외를 잡아서 회복시킬 수 있는 곳에만 넣으면 된다.
만약 당신이 부분적으로만 회복시킬 수 있다면, 당신은 본래의 예외를 다시 던질(throw) 수가 있다.
(빈 throw 구문 "throw;" 을 사용하라.)
스택은 다음번 일치하는 catch 블럭을 만날 때까지 계속 풀릴 것이다.
Examples
예외처리는 디버그빌드와 릴리스빌등 양쪽에서 예외조건을 추적하기위한 곳에서 가장 훌룡하게 사용된다.
만약 적절히만 사용된다면,
예외처리들은 자동 해제(clenaup)와 애플리케이션을 종료시키거나 그자신을 다시 유효한 상태로 되돌릴수 있다.
이와같이 예외처리는 릴리스 코드를 위해 완벽하다.
왜냐하면 그들은 예상치않은 에러가 생길지 모르는 잘 수행되는 프로그램에
실제 사용자가 원하는 모든것들은 제공한다. 정확하게 말해서 예외처리는 다음과 같은 장점들을 가진다.
- 실행중 어떤 위치에서라도 모든 자원들의 자동 해제.
- 응용프로그램이 강제종료되게 하거나 유효한상태로 복귀되게 강제한다.
- 선택적이 아니라 (반환값이나 assertions 과 같이), 예외의 수령자가 그것을 처리하도록 강제한다.
- 할당(allocation) 코드를 작성한 사람이 해체(deallocation) 코드를 작성하고, 암시적으로 처리할 수 있게한다.
(물론, 파괴자를 말하는 것이다!)
추가적인 부하 때문에 예외처리는 일반적으로 일반적인 제어 흐름에 사용하는것은 좋은 생각이 아니다.
왜냐하면 다른 제어 구조들이 더 빠르고 효율적이기 때문이다.
방법 3 - 반환값
장점: 내장타입이나 정수 사용할 때 빠르고, 클라이언트의 논리를 변경할 수 있게 해주며 청소(cleanup)가 가능하다.
단점: 에러처리가 의무적이 아니며, 값들이 혼란스러울 수가 있다.
설명
기본적으로, 우리는 유효한 데이터를 호출자에게 돌려주던가, 아니면 에러임을 나타내는 값을 돌려준다.
const int divide(const int divisor, const int dividend)
{
if( dividend == 0 ) return( 0 );
// avoid "integer divide-by-zero" error
// "정수 0으로 나눔" 에러를 피한다.
return( divisor/dividend );
}
영(0)이란 값은 함수가 실패했음을 나타낸다.
불행히도 이 예제에서는 또한 피젯수에 0을 넘겨줬을 때 0이라는 반환값을 얻을 수도 있다.
(완전히 유효한 연산임에도 불구하고.)
따라서 호출자는 이 함수가 에러를 반환했는지 아닌지 도대체 알 수가 없다.
이 함수는 말이 안되지만, 에러처리를 위해 반환값을 사용할 때의 문제를 보여준다.
모든 함수에서 통하는 에러값을 택한다는 건 어렵거나 불가능하다.
결론
반환값들은 에러 처리와 로직의 변경이 혼재된 상황에 유용하다.
예를 들어서 하나의 함수가 돌려준 반환값이 특정한 값(일반적으로 0)이면 에러로 간주하고,
그 이외의 값이면 그 값을 어떠한 처리에 사용하는 경우를 생각할 수 있을 것이다.
이러한 방식에는 함수를 호출하는 쪽이 함수가 돌려준 반환값의 의미를 제대로 알고 있으며
그에 따른 적절한 처리를 할 것이라는 가정이 깔려 있다.
방법 4 - 로깅
디버거를 사용할만한 상황이 아닌 경우 에러들을 파일에 기록하는 것이 디버깅에 커다란 도움이 되기도 한다.
이 방법은 하나의 전역 로그 파일을 선언하고(또는 std::clog를 사용),
에러가 발생할 때마다 그것에 대한 메시지를 파일에 기록하는 것이다.
에러 메시지에 에러가 발생한 파일 이름과 행 번호를 포함시키는 것도 좋다.
현재 컴파일되는 파일 이름과 행 번호는 __FILE__과 __LINE__ 로 삽입할 수 있다.
또한 에러 이외의 정보도 로그 파일에 기록할 수 있다.
그러한 정보에는 행렬의 최대 개수라던가 기타 디버거로는 접근할 수 없는 데이터가 포함된다.
또는 오류를 유발한 데이터를 출력할 수도 있을 것이다.
그런 경우라면 std::fstream이 큰 도움이 된다.
조금 노력을 들인다면 어써션이나 예외가 발생했을 때 그에 대한 메시지를 기록하게 만들 수도 있을 것이다.
이 방법의 장점은 다음과 같다.
- 기존 코드와 쉽게 통합되며, 이식성이 높다.
- 사람이 이해하기 쉬운(디버거에 비해) 디버깅 정보를 얻을 수 있다.
- 프로그램 실행을 방해하지 않고도 자세한 정보를 얻을 수 있다.
- 물론 파일 작업에 따른 부담이 추가되므로, 이 방법에 의한 이점이 그러한 부담을 상쇄할 수 있을 것인가를 잘 따져봐야 할 것이다.
한 가지 더...
이 문서를 여기서 끝내려고 했었지만, 디버깅에 대한 복합적인 접근이 얼마나 가치있는 것인지를 보이려고 한다.
이를 위한 가장 쉬운 방법은 되도록이면 비 - C++ 코드와도 동작해야 하는 간단한 클래스를 만드는 것이다.
파일 클래스가 좋은 예이다.
간결함과 이식성을 위해 C 의 fopen() 과 관련된 함수들을 사용할 것이다.
다음과 같은 요구 사항들을 만족해야 한다.
- 파일 포인터의 라이프타임(lifetime)과 일치하는 생성자와 소멸자.
- 각 함수들이 가지고 있는 가정들을 기술하기 위한 assertion 들.
- 클라이언트가 예외적인 조건을 처리할 수 있도록 하는 exception 타입들.
- 데이타를 읽고 쓰기 위한 멤버 함수들.
- 스택-기반의 데이타를 손쉽게 처리할 수 있는 템플릿화된 멤버 함수들.
- fail-safe 소멸자와 책임이 따르는 멤버 함수들을 포함하는 예외 안전성(exception safety).
- 이식성.
코드는 다음과 같다.
#include <cstdio>
#include <cassert>
#include <ciso646>
#include <string>
class file
{
public:
// Exceptions
struct exception {};
struct not_found : public exception {};
struct end : public exception {};
// Constants
enum modes { relative, absolute };
file(const std::string& filename, const std::string& parameters);
~file();
void seek(const unsigned int position, const enum modes = relative);
void read(void* const data, const unsigned int size);
void write(const void* const data, const unsigned int size);
void flush();
// Stack only!
template <typename T> void read(T& data) { read(&data, sizeof(data)); }
template <typename T> void write(const T& data) { write(&data, sizeof(data)); }
private:
FILE* pointer;
file(const file& other) {}
file& operator = (const file& other) { return( *this ); }
};
file::file(const std::string& filename, const std::string& parameters) : pointer(0)
{
assert( not filename.empty() );
assert( not parameters.empty() );
pointer = fopen(filename.c_str(), parameters.c_str());
if( not pointer ) throw not_found();
}
file::~file()
{
int n = fclose(pointer);
assert( not n );
}
void file::seek(const unsigned int position, const enum file::modes mode)
{
int n = fseek(pointer, position, (mode == relative) ? SEEK_CUR : SEEK_SET);
assert( not n );
}
void file::read(void* const data, const unsigned int size)
{
size_t s = fread(data, size, 1, pointer);
if( s != 1 and feof(pointer) ) throw end();
assert( s == 1 );
}
void file::write(const void* const data, const unsigned int size)
{
size_t s = fwrite(data, size, 1, pointer);
assert( s == 1 );
}
void file::flush()
{
int n = fflush(pointer);
assert( not n );
}
int main(int argc, char* argv[])
{
file myfile("myfile.txt", "w+");
int x = 5;
int y = 10;
int z = 20;
float f = 1.5f;
float g = 29.4f;
float h = 0.0129f;
char c = 'I';
myfile.write(x);
myfile.write(y);
myfile.write(z);
myfile.write(f);
myfile.write(g);
myfile.write(h);
myfile.write(c);
return 0;
}
만약 위 코드를 윈도우즈상에서 컴파일한다면, 프로젝트타입을 "Win32 Console App"으로 선택해야 한다.
이 클래스가 클라이언트들에게 어떠한 잇점을 제공해 주는가?
- 사용자가 어떤 함수가(그리고 그 함수의 어떤 어떤 인자가) 멤버함수를 실패하게끔 만들었는지 확인하기 위해 손수 디버그할 수 있도록 한다.
- 모든 리턴값과 대부분의 인자들을 디버그 빌드시에 체크한다.
- 만약 예외상황이라면 두 종류의 예외를 던진다. 예외가 던져졌을 때 무엇을 할 것인지는 클라이언트가 결정한다(설치복구 마법사를 실행하고 시디로부터 몇몇 데이타 파일들을 복구하고, 사용자입력을 받고 등등).
- 대부분의 일반적인 작업들에 대한 손쉬운 방법을 제공한다.
- 복사 생성과 복사 할당을 허용하지 않는데, 이것은 처리할 준비가 안되었다.
예외들은 파일 포인터의 상태에 매우 중대한 두가지 지점에서 던져진다는 것을 주의하라:
- 파일이 만들어졌을때, 성공하리라고 확신한다.
- 파일이 읽혀질때, 예상치 못한 파일끝을 만나지 않으리라고 확신한다.
오버헤드
assertion 에 의해서 평가(evaluation)될 리턴값을 담고 있는 모든 변수들은
릴리즈 빌드시에 최적화 컴파일러에 의해 제거된다.
릴리즈 빌드시에는 assertion 들이 평가되지 않는다.
이것은 프로그램이 매우 작은 오버헤드를 가지고 실행된다는 것을 의미한다.
위 소스 코드의 멤버함수를 보면, 릴리즈 빌드시에는 기본적으로 함수호출로 평가되는 것을 볼 수 있다.
하지만, 예외(exception)는 릴리즈 빌드시에도 존재하는데,
예외는 원래 프로그램 실생 시점에서 프로그램의 안정성과 복구 기능을 제공하는데 쓰이는 것이므로 당연한 것이다.
멤버 함수 안에서는 assert( this )나 assert( pointer ) 같은 것을 사용할 필요가 없다.
왜냐 하면, 파일 상태에 뭔가 문제가 있다면 파일 객체가 아예 생성되지 않았을 것이기 때문이다.
만약 new 연산자가 파일 객체를 제대로 할당하지 않는다면,
생성자/소멸자는 결코 호출되지 않을 것이며, new 연산자는 예외(exception)를 던진다.
만약 fopen 이 유효한 파일 포인터를 되돌리지 않는다면, file::not_found 예외가 던져지며,
그러면 파일 소멸자도 호출되지 않으며 어떠한 어떤 멤버 함수도 사용되지 않을 것이다.
그렇기 때문에 유효하지 않은 this 포인터나
유효하지 않은 파일 포인터를 멤버 함수에 사용할 지 모른다는 걱정은 절대 할 필요가 없다.
(만약 여러분이 널 포인터를 사용해서 멤버 함수를 호출하려 한다면, this 는 아마 널일 것이고 멤버 함수를 접근하는 것은 아마 접근 위반(access violations)을 초래할 것이다. 프로그래머라면 누구든 그런 경험을 해 보았을 것이다.)
만약 멤버 함수를 헤더 파일속으로 집어 넣고, inline 키워드를 사용했다면,
컴파일러는 그 함수들을 릴리즈 빌드시에 인라인 함수로 만들게 되며,
그러면 함수 호출 오버헤드와 관련된 부가 작업이 모두 제거될 수 있다.
결과적으로 릴리즈 빌드에서는 마치 단순히 순수한 C 코드만을 사용한 것만큼 빠른 클래스를 만들 수 있게 된다.
assertion 을 잘 사용하면 최종(릴리즈) 빌드의 크기나 속도를 저하시키지 않고 코드를 더 쉽게 디버그할 수 있게 한다.
결론
파일 클래스로 설명한 테크닉들은 내부 객체에 대한 포인터나
핸들만을 외부에 노출시키는 대부분의 상속된(legacy) 코드에 사용될 수 있다.
파일 클래스의 기능성을 확장하는 것은 독자의 숙제로 남겨 두겠다.
복사 생성자를 추가하고 배정 연산자를 중복(overload)한 후 여러 가지 상황에서 테스트해 보기 바란다.
클래스를 수정했을 때 어떤 assertions들이 실패로 평가되는지,
그리고 어떤 assertion들이 새 코드의 에러를 잡는데 도움이 되는지 살펴보면 재미있을 것이다.
코드를 수정함에 따라 파일 객체가 유효하지 않은 상태가 되는 일도 있을 것이다.
절대로 실패하면 안되는 함수들에 대해서는 assertion을 사용할 것.
예외는 릴리즈 코드의 최후의 방어선으로 사용할 것.
그리고 반환값은 원래 목적 그자체, 즉 호출한 곳에 결과를 돌려주는 것을 기본으로 할 것.
프로그램 실행 도중에 뭔가를 점검할만한 상황이 아니라면 로그를 사용할 것.
이와 같은 제안들을 잘 지킨다면 이식성이 좋은 C++ 코드를 만들 수 이다.
그러나, 누구나 C++ 코드를 작성하는 것은 아니며,
또한 클라이언트가 소스 코드를 직접 읽고 수정하도록 강요할 수는 없는 상황도 있다.
디버깅 방법은 자신의 상황, 그리고 더욱 중요하게는 클라이언트의 상황을 고려해서 현명하게 선택해야 할 것이다.
C++ 코드와 비 C++ 코드 사이의 유일한 정보 교환 수단이 오직 반환값 밖에 없는 상황이 생기기도 한다.
모든 반환값들을 일일이 점검하는 것은 매우 고통스러운 일이다. 그럴 때 assertion이 그러한 고통을 덜어주기도 한다.
'[ Programing ] > C++' 카테고리의 다른 글
[Debug] MS C++ 디버깅 매직넘버 (0) | 2011.04.16 |
---|---|
Function pointer (0) | 2010.07.01 |
class 속성 private, protected, public 효율적 사용법. (0) | 2010.02.24 |
Function & Method Name 정리 (0) | 2010.02.18 |
전처리문의 종류(#include, #define, #ifdef, ... ) (1) | 2010.02.10 |