출처:http://webdizen.new21.net/blog/3110
Are you looking for a way to speed up the debugging process in C++? This article explains how to use asserts to do just that, showing that not all macros are evil.
If a man begins with certainties, he shall end in doubts;
But if he will be content to begin with doubts,
He shall end in certainties.
[Francis Bacon 1561-1626]
Assertive ProgrammingIf there is one thing I have learned over the past few years, it is not to underestimate the power of assert(). It comes with your compiler and can be found either in cassert or assert.h.
The reason I love assert is because it looks after me and helps me find bugs I was sure weren’t there. I mean, we all write bugfree software, right? Yeah right. Well there have been many Kodak moments when an assert fired on something I was dead sure could never happen; I should be a rich man as the look on my face was priceless.
If you are not familiar with asserts, you should start using them right now. Use assert to check that your code is working the way you expect it to or be prepared to pay the price. No not for my face, but for long nights behind the debugger, ticking away possible problem sources until you have your own Kodak moment.
When do you use assert, you ask? Simple… whenever you can use it to verify the truth of a situation: ‘this pointer can never be null’, ‘this number is never smaller than zero’, ‘there is always at least one customer stored in this list’, ‘this code won’t be used the next millennium, two digits are fine’.
Use proper error handling when you can check for things that can go wrong. Use asserts on things you are sure can’t go wrong.
Trust me… the sillier the assert… the more valuable it is. I tested this once myself, grinning, thinking ‘this is ridiculous, want to bet it will never fire?’, only to have it triggered a couple of months later! And because the assert was so preposterous and silly I immediately knew which conditions were breaking my code.
assert.
- To state or express positively, affirm: asserted his innocence
- To defend or maintain (one’s rights, for example)
The important thing to remember is that asserts are only compiled into your code when the NDEBUG macro is not defined. So in the final optimized version of your application, they are not there to bloat or to slow things down! The only investment you have to make is typing them out while you are working on those classes and functions. It will pay you back greatly when it helps you shorten the time it takes to track down bugs.
Face it… we all write code on assumptions we make about the situation the code will be running in. Assert you are in that situation, because your code base will grow beyond the picture you have of it in your mind.
Though we will be implementing our customized version of assert here, all you need to do is include assert.h and you are ready to use the assert macro that comes with it:
// assert.h
#ifndef NDEBUG
void __assert(const char *, const char *, int);
#define assert(e) \
((e) ? (void)0 : __assert(#e,__FILE__,__LINE__))
#else
#define assert(unused) ((void)0)
#endif
The __assert helper function prints an error message to stderr and the program is halted by calling abort(). It is possible that the implementation that comes with your compiler varies slightly, but you get the idea.
Lets use a simple example function:
void writeString(char const *string) {
assert( 0 != string );
...
}
In the unfortunate situation that the pointer to string is NULL, execution will halt and you will be offered the possibility of opening the debugger and jumping to the location in the source where the assertion failed. This can be very handy as you can examine the call stack, memory, registers, and so forth, and are likely to catch the perpetrator red handed!
Although I am ranting about why you should use assert, this article is aimed at showing you how to implement your own version using preprocessor macros.
Here is a very basic version that doesn’t even halt execution:
#ifndef NDEBUG
# define ASSERT( isOK ) \
( (isOK) ? \
(void)0 : \
(void)printf(“ERROR!! Assert ‘%s’ failed on line %d ” \
“in file ‘%s’\n”, #isOK, __LINE__, __FILE__) )
#else
# define ASSERT( unused ) do {} while( false )
#endif
Notice that we don’t need a helper function to get information onto the screen. (I am sticking to printf in the following examples, but there is nothing stopping you from using fprintf and stderr instead.) The stringize macro operator (#) names the condition for us in the printf statement, adding quotes as well and the predefined __LINE__ and __FILE__ macros help to identify the location of the assert.
The do { } while ( false ) statement for the unused version of assert, I used to make sure that the user cannot forget to conclude his assert statement with a semicolon. The compiler will optimize it away, and I consider it a slightly better way to say that I am not doing anything.
We are only one step away from halting our application’s execution (don’t forget to include stdlib.h):
#ifndef NDEBUG
# define ASSERT( isOK ) \
if ( !isOK ) { \
(void)printf(“ERROR!! Assert ‘%s’ failed on line %d “ \
“in file ‘%s’\n”, #isOK, __LINE__, __FILE__) ); \
abort(); \
}
#else
# define ASSERT( unused ) do {} while ( false )
#endif
In case writeString is called with a NULL pointer now, we are told what went wrong, where it went wrong and the application is halted.
When you are using the Microsoft Visual C++ 7.1 compiler, you’ll be presented with a dialog screen:
After pressing Retry you are presented with more dialogs and finally... you end up in the debugger.
The problem with abort() is that it doesn’t let you resume execution after you’ve concluded that the assert was benign; or maybe you are interested in what happens immediately after the assert? You need to disable the assert (never a good thing), recompile and restart your application.
There are other ways to halt execution and you will have to look in your compiler’s documentation to discover what the correct call is, but with Visual C++ it’s an interrupt call:
__asm { int 3 }
When our application halts again on the writeString function, we’ll encounter the following dialog (did you recognize it? We actually came across this dialog when halting the application with the abort() function!) :
It is not a problem to continue execution now, if we think this is responsible… that is you are often recommended to implement your own assert functionality.
Wouldn’t it be nice to be able to add some hints or remarks along with the location of the assert? When the assert fires and you don’t have a debugger available, this message might still tell you what the problem is:
void writeString(char const *string) {
assert(0!=string, “A null pointer was passed to writeString()!”);
...
}
The simplest solution is to just expand the assert macro and make it accept a message as well as a condition:
#ifndef NDEBUG
# define ASSERT( isOK, message ) \
if ( !(isOK) ) { \
(void)printf(“ERROR!! Assert ‘%s’ failed on line %d “ \
“in file ‘%s’\n%s\n”, \
#isOK, __LINE__, __FILE__, #message); \
__asm { int 3 } \
}
#else
# define ASSERT( unused, message ) do {} while ( false )
#endif
Again the stringize operator helps us to stuff the message we want into the printf statement. We could call it a day now, but I am a very lazy coder… I do not want to be forced to put messages into the assert, sometimes I just want to assert.
Of course it is possible to write an ASSERT(condition) and an ASSERTm(condition, message) macro, but did I mention I am a forgetful coder too? I’d much rather have a single ASSERT statement that can do both.
The first thing that comes to mind is the fact I could do this easily with a function:
void MyAssert(bool isOK, char const *message=””) {
if ( !isOK ) {
(void)printf(“ERROR!! Assert ‘%s’ failed on line %d “
“in file ‘%s’\n%s\n”,
__LINE__, __FILE__, message);
__asm { int 3 } \
}
}
So maybe if I declared another function:
void NoAssert(bool isOK, char const *message=””) {}
And then defined assert as:
#ifndef NDEBUG
# define ASSERT MyAssert
#else
# define ASSERT NoAssert
#endif
While this seems like a quick solution, I have completely lost my extra debug information! The line information is the same now for every assert… oh… the compiler substitutes __LINE__ with the actual line number it is compiling at that moment, and since we are making a function call – all line numbers lead to the MyAssert function!
Alexandrescu demonstrates a great way around this problem [Alexandrescu]. (It is also a great article showing how you can take assertions to a higher level after this one, by making them throw exceptions!)
#ifndef NDEBUG
#define ASSERT \
struct MyAssert { \
MyAssert(bool isOK, char const *message=””) { \
if ( !isOK ) { \
(void)printf(“ERROR!! Assert failed in “ \
“file ‘%s’\n%s\n”, __FILE__, message); \
__asm { int 3 } \
} \
} \
} myAsserter = MyAssert
#endif
For some reason my Visual C++ 7.1 compiler will not accept the __LINE__ macro next to the __FILE__ macro in the code above. The strange thing is that the __FILE__ macro works fine, but with __LINE__ it complains:
error C2065: '__LINE__Var' : undeclared identifier
It is never easy, but I do not want to give up at this point. Since the macro is expanded as a single line into the place where we are calling it, and since the compiler apparently has no objection to me assigning __LINE__ as a default parameter in a constructor, let's try again:
#ifndef NDEBUG
#define ASSERT \
struct MyAssert { \
int mLine; \
MyAssert(int line=__LINE__) : mLine(line) {} \
MyAssert(bool isOK, char const *message=””) { \
if ( !isOK ) { \
(void)printf(“ERROR!! Assert failed on “ \
“line %d in file ‘%s’\n%s\n”, \
MyAssert().mLine, __FILE__, message); \
__asm { int 3 } \
} \
}\
} myAsserter = MyAssert
#endif
Now that we have our line information back, we are nearly there; as soon as we add a second assert, the compiler complains that we are redefining the struct MyAssert! If only we could keep the struct declaration local… and again Alexandrescu shows us how [Alexandrescu]:
#ifndef NDEBUG
#define ASSERT \
if ( false ) {} else struct LocalAssert { \
int mLine; \
LocalAssert(int line=__LINE__) : mLine(line) {} \
LocalAssert(bool isOK, char const *message=””) { \
if ( !isOK ) { \
(void)printf(“ERROR!! Assert failed on “ \
“line %d in file ‘%s’\n%s\n”, \
LocalAssert().mLine, __FILE__, message); \
__asm { int 3 } \
} \
} myAsserter = LocalAssert
#else
#define ASSERT \
if ( true ) {} else struct NoAssert { \
NoAssert(bool isOK, char const *message=””) {} \
} myAsserter = NoAssert
#endif
There is a lot of fun to be had with macros and sometimes it is possible to create the wildest incantations with them [Niebler/Alexandrescu]. I hope you are convinced that despite the fact that they can be considered evil, there is something magical about them as well.
As a final example I will show you how to create personal and customizable debug streams in the next article… all with macros.
References
[STL] – The Standard Template Library
< comes standard with your compiler but this one is very portable>
[BOOST] – Boost C++ Libraries
[ACE] – The ADAPTIVE Communication Environment
http://www.cs.wustl/edu/~schmidt/ACE.html
[Niebler] – Eric Niebler
“Conditional Love: FOREACH Redux”
http://www.artima.com/cppsource/foreach.html
[Alexandrescu] – Andrei Alexandrescu
Assertions
'Language > C++' 카테고리의 다른 글
포인터의 용도 / 동적할당메모리 (0) | 2013.04.29 |
---|---|
<input> cin(), get(), getline() 함수 차이. (0) | 2011.04.03 |
ostream/istream (0) | 2011.03.14 |
연산자 오버로딩 (0) | 2011.03.14 |
C++ 에서 setw(int num) 함수를 이용하여 깔끔하게 출력하기! (0) | 2011.03.09 |