Home » Community » Forums » » C++ Debugging
  Intel sponsors gamedev.net search:   
[Control Panel] [Register] [Bookmarks] [Who's Online] [Active Topics] [Stats] [FAQ] [Search]

Add Forum to Favorites |  Send Topic To a Friend | View Forum FAQ | Track this topic


 Last Thread Next Thread 
 C++ Debugging
Post Reply 
Nice article, null. Good work. I really like the way you present the pros and cons of each method, and your conclusion that it's best to use a mixture is right on the mark.

If I can add one thing that wasn't mentioned under "exceptions": one drawback is that it's hard to debug code when one is thrown.

Example:
try
{
  func1 (); // could throw exception
  func2 (); // could also
  func3 (); // could as well
}
catch (exception &x)
{
  // exception handler
}
 

If you're running this code in the debugger, the best you can hope for is to set a breakpoint in the handler; you don't know (unless you describe it very well in your exception) from where the exception was thrown.

On the other hand, if you don't have a try/catch block, and you still allow the functions to throw exceptions, then as soon as one is thrown your debugger will halt and go to the line of code that threw the exception.

Because of this, I find myself putting the following code around my try/catch statements:
#ifndef __DEBUG
try {
#endif
This looks really tacky, but it's a help when debugging.



 User Rating: 1100   |  Rate This User  Send Private MessageView ProfileView Journal Report this Post to a Moderator | Link

A great article about catching programming and design errors! One thing I think you missed was another important class of errors: system and user errors. You know, the normal stuff that comes up as you go about daily business: file not found, not enough memory, invalid user input, can't find printer, etc.

Most people check the result of the basic system functions (files, memory, resources) by returning immediately from the function, sometimes without properly releasing allocated objects or other resources. Exceptions help enforce the cleanup portion, but rigorous error detection and reporting should always be a conscious design decision.

As with developer errors, the failure path for system and user errors aren't exercised very often -- usually just once or twice before you realize that you forgot rename a necessary file or something equally innocuous. A complete QA cycle *should* stress test each possible edge case and input limitation, but how many of us have a full QA team. =)

Basically, my point is that you should put as much thought into detecting, propogating, and reporting errors to the user as you do in your own debugging. They are going to be stuck with your software *alot* longer than you are, and they are going to do some crazy things to it (you mean that's not a cupholder?). When you have to debug some user's problem over the phone or via email -- a single error code with the exact file and line number will make your day alot better, and reduce tech support costs over time. Best of all, you can use the same debugging techniques discussed in the article!

 User Rating: 1015   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Good article.

A couple of comments about the exception stuff:

Exceptions shouldn't be used as flow control structures because they are 'non-local forms of error handling' ( paraphrased from the Stroustrup book ).

Basically its poor design, and would become very hard to follow and debug if you use exceptions as flow control. I've had to deal with code written like that, its a real pain.

As far as overhead, I think most of the overhead comes from stack unwinding, at least in the non-exceptional case. Because this has to happen weather or not you use exceptions its a fixed cost in using C++ ( except maybe when you have a scope which has non local variables or only built-in types )

But as far as throwing an exception having a lot of overhead ( including the mandatory copying of an object ) I say, fine by me. Throwing exceptions should only be done under exceptional conditions.

One other thing about exception handling is that if you want to have your unit tests test the exceptional code paths through functions, methods, etc. the amount of effort to do so can be very high. Although it does provide a good payoff. The latest C/C++ User's Journal has a good in-depth artical about adding exception testing to unit tests.

On Logging

I think maybe a follow up article on real-time logging might be cool. The Game Programming Gems ( is that the correct title? ) book has a chapter on logging too.

Debugging real-time systems is probably the hardest kind of debugging.

 User Rating: 1015   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Pretty good article. I'd add that another BIG source of bugs is the 'ol Cut-and-Paste, which is an endless source of errors.

However, unless there's some new C++ rules that I don't know about, the sort functions in the Assert section are screwed up. Checking the sizeof a pointer parameter (whether it points to an array or not) will always return the sizeof the pointer. It will never return the # elements in the array, or the # of bytes in an array (except for local arrays).

 User Rating: 1015    Report this Post to a Moderator | Link

Nice article indeed.

However I spotted a minor error on page 2 (Assertions) in the examples section in the second example:

(quote)

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]);

You see, that innocent-looking algorithm won't work if:

The pointer is null, or
sizeof(myarray) cannot be used to determine the number of elements in the array, either because the array was not allocated on the stack, or because someone has passed in the address of a single (non-array) object.

(endquote)

The example and the explanation below it are misleading, because in this case sizeof can _never_ be used to determine the size of an array, as sizeof is evaluated at compile time and therfore doesn't know or care about the parameters used in the function call that activates the function. The compiler will use the formal parameter type to evaluate the sizeof(myarray) which happens to be equal to sizeof(int*) in the above example.

In fact if you tried to compile the above code your compiler would probably warn you about the second assert.



 User Rating: 1015   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Some points:
- I think you failed to make the important destinction between software defects (bugs) and runtime errors clear, i.e. you mixed methods appropriate for detecting either kind of error. assertions for bugs, exceptions for runtime errors

- the example with 'const int' thrown as an exception is not very well chosen, you should only throw proper exception classes, otherwise you'll get in trouble with the exception technique used for normal control flow or just with several overlapping error ids. Exceptions for normal control flow are (just like goto) normally a bad idea, like a previous poster quoting Stroustrup already pointed out, but I think it was also Stroustrup who gave an example where it might be appropriate.

- AFAIK uncaught exceptions call terminate() instead of unwinding the stack and then magically passing main...

- your notion that exceptions provide 'automatic cleanup' is very dangerous. Writing exception safe code is non-trivial at best. See Stroustrup 'The C++ Programming language' 3rd Appendix E for examples. There are many more articles and books dealing with this particular issue, so it's unfortunately not 'automatic' - at least not automatically correct

- speed of the exception handling mechanism: Stroustrup points out that the speed hit is neglectible and that implementors can even implement the try block so it doesn't incur any runtime penalty. In the case of a thrown exception speed is no longer an issue anyways.

- exceptions are the (only?) elegant way of reporting errors from constructors

- stressing the importance of well defined class invariants for fault-tolerant code would have been nice. They are one important criteria for exceptions to function properly, also a place to put assertions to good use.

regards,

BuschnicK


Life would be much easier if I had the source code.
http://home.t-online.de/home/BuschnicK/

 User Rating: 1015   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

BuschnicK:

Uncaught exceptions DO propagate up the call chain performing stack unwinding appropriately. This is a requirement for them to work properly, otherwise you would need a try/catch block everywhere.

They go up to the originating thread's entry point. WinMain or main in the case of the main thread. Whether or not they properly call dtors of automatic objects declared in main() might depend on how good the compiler is.

I think that uncaught exceptions that occur in other threads terminate the app. In fact, usually in a not-so-nice kind of way.
So long as you have a try/catch block in main() and in every thread routine, things should be ok.


 User Rating: 1015   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Yeah sorry, I've been unclear on that point. I just meant that they don't just drop through main or something. Of course they do unwind the call stack and call terminate( ) afterwards. (correct?)

>
main in the case of the main thread. Whether or not they properly call dtors of automatic objects declared in main() might depend on how good the compiler is.
<

hmm, I think the standard says all properly/fully constructed objects are to be destroyed, so a compiler missing them in main would be broken - no?

>
I think that uncaught exceptions that occur in other threads terminate the app. In fact, usually in a not-so-nice kind of way.
<

I have no idea how multithreading and exceptions work together... Stroustrup only briefly mentions they are 'compatible', but since multithreading support is not part of the language he doesn't elaborate further.
I can imagine writing both exception and thread safe code at the same time to be quite difficult though... Lots of nasty gotchas to be aware of.

I just realized I sounded quite negative about the article - I'm not! I really appreciate the work and fully support the idea to give more thought to error handling. Teaching good software engineering practices is a good idea!

Please take it as it was meant to be: constructive criticism.

regards,

BuschnicK

Life would be much easier if I had the source code.
http://home.t-online.de/home/BuschnicK/

 User Rating: 1015   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

In section Method 1 - Assertions, you give the following incorrect code examples:


FILE* p = 0;
assert( p = fopen("myfile.txt", "r+") );


...because fopen will not be called in the release version! This is the correct way to do it:

FILE* p = fopen("myfile.txt", "r+") );
assert( p );



The "correct" code leaves fopen() is the release version, but the release version now has NO run-time error checking! There is a difference between run-time errors and bugs. A missing file is (probably) a valid run-time error, not a programmer mistake.


 User Rating: 1015    Report this Post to a Moderator | Link

In section Method 1 - Assertions, you give the following incorrect code examples:


FILE* p = 0;
assert( p = fopen("myfile.txt", "r+") );


...because fopen will not be called in the release version! This is the correct way to do it:

FILE* p = fopen("myfile.txt", "r+") );
assert( p );



The "correct" code leaves fopen() is the release version, but the release version now has NO run-time error checking! There is a difference between run-time errors and bugs. A missing file is (probably) a valid run-time error, not a programmer mistake.


 User Rating: 1015    Report this Post to a Moderator | Link

In section Method 1 - Assertions, you give the following incorrect code examples:


FILE* p = 0;
assert( p = fopen("myfile.txt", "r+") );


...because fopen will not be called in the release version! This is the correct way to do it:

FILE* p = fopen("myfile.txt", "r+") );
assert( p );



The "correct" code leaves fopen() is the release version, but the release version now has NO run-time error checking! There is a difference between run-time errors and bugs. A missing file is (probably) a valid run-time error, not a programmer mistake.


 User Rating: 1015    Report this Post to a Moderator | Link

Really a good article.
This helps beginner programmers and masters to handle their program errors in a right way.


 User Rating: 1015    Report this Post to a Moderator | Link

All times are ET (US)

Post Reply
 Last Thread Next Thread 
Forum Rules:
You may not post new threads
You may post replies
You may not edit your posts
You may not use HTML in your posts
Jump To:
Administrative Options: