Thursday, August 9, 2012

Debugging C++ (Part 1): How to write less bugs

Hi all! I planed to make a post about some different methods to debug a C++ program. And I realized that it will be a very long post, so I split it into 4 parts. The first part is about some methods to avoid dummy bugs notably using two new C++11 features. The second is a presentation of valgrind and gdb. I include in the gdb part an introduction to the reverse-debugging that consists in running the program backward. The third part is an explanation of the usage of dmesg to find an instruction in a library that leads to a segmentation fault. One of the advantage is that it works after the crash, and don't need to restart the program. The fourth and last part is about the print method. I present a way to make this method pleasant to use and easy to maintain.

This is far from being an exhaustive list of debugging methods, just some of my favorites. You are invited to share yours in comments! :-)

This first post presents the importance of using warnings when compiling, assert to verify the coherence of the program, and miscellaneous things introduced by C++11.

1 Warnings

The first thing to do to avoid stupid bugs is to think before writing any piece of code. It can be hard sometimes, but it's totally worth it.

My global philosophy about programming is that I want my computer to insult me whenever he can. I want a compiler able to detect as many errors as possible.

So, the thing to do in the aim to make the compiler as hard as possible, is to enable warnings. Personally, on g++ I always use -W, -Wall, -Wextra and -Werror for changing all the warnings into errors. This can save some hours of debugging. Let's see an example of a buggy code that compiles without warning, but with enable warnings it won't and it is great!

int i = -42;
unsigned int j = 51;

if (i > j)
     // Bug found.

It can be disappointing that i > j is evaluated as true. It is due to an implicit conversion. The i once compared with an unsigned int is converted into a unsigned int equal to UINT_MAX - 41. So this is really easy to make this error when the type are declared too early and you forgot what is the type of i and j. Warnings are just mandatory! I hope this little example is enough to convince you. I'm sure there are several hundred of examples like this one, and you just have to run through the net to find out another examples.

2 Assert

A good practice is to use the macro assert available in the header cassert. This is a macro that evaluates its content and stops the program if its content is evaluated to false. If you define NDEBUG (the common way is to pass the -DNDEBUG option to g++, -D allows to define a macro), the code inside the parenthesis of the macro isn't evaluated.

The main interest of assert is that it can be a good checker for preconditions or postconditions. Beware, you must not use it as a way to manage run time error. It is here to verify all along of your development that you are not receiving something weird. If you use well assert, it must stop the flow of your program before it starts acting crazily. By making this, you ensure looking at the good spot for finding the source of the problem, and not to a side effect that occurs 20 functions later. This can reduce considerably the debugging time.

As said above, the code between the parenthesis isn't evaluated in release mode. So a bad use of assert would be to put real code in it. Because once released, this code will not be ran. It is also its advantage. Checking all these preconditions can introduce an overhead, but you don't have to worry about it in release mode since this is like this code never exists.

As a little conclusion, if you don't already use assert, start now! :) It can change a lot of things and it has already saved a lot of debugging hours for me. I hope it will be the same for you!

3 Miscellaneous

3.1 Preventing Narrowing

Now I will give some little tips which can help. There are a lot of tips like this. Once again, I invite you to leave your own tips in the comments!

A common problem in C or C++ is narrowing. Preventing this is an addition of the C++11, which can prevent a lot of bugs. As an example:

void doit(int);

int main()
  float i = 4.2;

  doit(i); // Huum... A bug hard that could be hard to find.
  doit({i}); // warning: narrowing conversion of 'i' from
             // 'float' to 'int' inside { } [-Wnarrowing]

This examples shows how it can help to avoid some kind of bugs. I recommend using it around all the variables you want to protect. These situations happens, and why not use the language to help you to not losing your time?

3.2 nullptr

It is also important to use strong typed variable. It helps the compiler to help you! Once again, C++11 comes with a strongly type null pointer nullptr. NULL is just 0 (see Stroustrup FAQ). And it can lead to bugs related to the dispatch on overloaded function.

void print(long int i) {  std::cout << "long int: " << i << std::endl; }

void print(int* i)     {  std::cout << "pointer" << std::endl;         }

int main()
  long int i = 51;

  print(i);       // prints "long int: 51"
  print(NULL);    // Raises a compile-time warning and prints "long int: 0"
  print(nullptr); // prints "pointer"

The warning is "passing NULL to non-pointer argument 1 of 'void print(long int)'". Hopefully there is a warning in this case because this is not the wanted comportment. The introduction of nullptr allows to represent the concept of a null pointer and to have it strongly and correctly typed. I think it is a good idea to use it instead of the NULL or 0.

3.3 Yoda Condition

I use this name after reading this very funny post about new programming jargon. This goal is for people who makes typo when they write like writing = instead of ==. I have to admit, I have rarely something like this written in my code since I don't use magic number (constant values written in the source in the middle of the code). But sometimes it can help. Here is an example:

int main()
  int i = 51;

  if (i = 51)
    std::cout << "Oops" << std::endl;

  if (51 = i)
    std::cout << "Thanks g++!" << std::endl;

Since some people want to write assignment in their conditions, the compiler can't warn about this. So you have to make it scream by helping him. In the second if we get an error "error: lvalue required as left operand of assignment".

That's all for the little tips, I hope you see why being drastic with yourself can help you. Writing these asserts is longer than not writing them because you have to think to all the precondition needed etc. But I can assure you that you are so happy when you see your program crash because of an assert and not with a segmentation fault or some crappy things like that. About the warnings, at the first glance, it seems annoying to be warns about everything, but programming is made of little details too. So use it! :)

For the miscellaneous tips, this is just little habits to take that can improve the work flow by reducing little mistakes. The last one is more a funny thing than a strong guideline as are using {} to prevent narrowing and nullptr to help the compiler by saying that we use a pointer.

Don't hesitate to post your own tips in comments ;)

1 comment: