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 ;)
This is cool!
ReplyDelete