Search

3.4 — Basic debugging tactics

In the previous lesson, we explored a strategy for finding issues by running our programs and using guesswork to home in on where the problem is. In this lesson, we’ll explore some basic tactics for actually making those guesses and collecting information to help find issues.

Debugging tactic #1: Commenting out your code

Let’s start with an easy one. If your program is exhibiting erroneous behavior, one way to reduce the amount of code you have to search through is to comment some code out and see if the issue persists. If the issue remains, the commented out code wasn’t responsible.

Consider the following code:

Let’s say this program is supposed to print the names the user enters in alphabetical order, but its printing them in reverse alphabetical order. Where’s the problem? Is getNames entering the names incorrectly? Is sortNames sorting them backwards? Is printNames printing them backwards? It could be any of those things. But we might suspect doMaintenance() has nothing to do with the problem, so let’s comment it out.

If the problem goes away, then doMaintenance must be causing the problem, and we should focus our attention there.

However, if the problem persists (which is more likely), then we know doMaintenance wasn’t at fault, and we can exclude the entire function from our search. This doesn’t help us understand whether the actual problem is before or after the call to doMaintenance, but it reduces the amount of code we have to subsequently look through.

Don’t forget which functions you’ve commented out so you can uncomment them later!

Debugging tactic #2: Validating your code flow

Another problem common in more complex programs is that the program is calling a function too many or too few times (including not at all).

In such cases, it can be helpful to place statements at the top of your functions to print the function’s name. That way, when the program runs, you can see which functions are getting called.

Tip

When printing information for debugging purposes, use std::cerr instead of std::cout. One reason for this is that std::cout may be buffered, which means there may be a pause between when you ask std::cout to output information and when it actually does. If you output using std::cout and then your program crashes immediately afterward, std::cout may or may not have actually output yet. This can mislead you about where the issue is. On the other hand, std::cerr is unbuffered, which means anything you send to it will output immediately. This helps ensure all debug output appears as soon as possible (at the cost of some performance, which we usually don’t care about when debugging).

Consider the following simple program that doesn’t work correctly:

Although we expect this program to print the value 4, it will actually print different values on different machines. On the author’s machine, it printed:

00101424

Let’s add some debugging statements to these functions:

Tip

When adding temporary debug statements, it can be helpful to not indent them. This makes them easier to find for removal later.

Now when these functions execute, they’ll output their names, indicating that they were called:

main() called
00101424

Now we can see that function getValue was never called. There must be some problem with the code that calls the function. Let’s take a closer look at that line:

Oh, look, we forgot the parenthesis on the function call. It should be:

This will now produce the correct output

main() called
getValue() called
4

And we can remove the temporary debugging statements.

Debugging tactic #3: Printing values

With some types of bugs, the program may be calculating or passing the wrong value.

We can also output the value of variables (including parameters) or expressions to ensure that they are correct.

Consider the following program that is supposed to add two numbers but doesn’t work correctly:

Here’s some output from this program:

Enter a number: 4
Enter a number: 3
4 + 3
The answer is: 9

That’s not right. Do you see the error? Even in this short program, it can be hard to spot. Let’s add some code to debug our values:

Here’s the above output:

Enter a number: 4
main::x = 4
Enter a number: 3
main::y = 3
4 + 3
main::z = 9
The answer is: 9

Variables x and y are getting the right values, but variable z isn’t. The issue must be between those two points, which makes function add a key suspect.

Let’s modify function add:

Now we’ll get the output:

Enter a number: 4
main::x = 4
Enter a number: 3
main::y = 3
add() called (x=4, y=5)
main::z = 9
The answer is: 9

Variable y had value 3, but somehow our function add got the value 5 for parameter y. We must have passed the wrong argument. Sure enough:

There it is. We passed the literal 5 instead of the value of variable y as an argument. That’s an easy fix, and then we can remove the debug statements.

One more example

This program is very similar to the prior one, but also doesn’t work like it should:

If we run this code and see the following:

Enter a number: 4
Enter a number: 3
The answer is: 5

Hmmm, something is wrong. But where?

Let’s instrument this code with some debugging:

Now let’s run the program again with the same inputs:

main() called
getUserInput() called
Enter a number: 4
main::x = 3
getUserInput() called
Enter a number: 3
main::y = 2
add() called (x=3, y=2)
main::z = 5
printResult() called (z=5)
The answer is: 5

Now we can immediately see something going wrong: The user is entering the value 4, but main’s x is getting value 3. Something must be going wrong between where the user enters input and where that value is assigned to main’s variable x. Let’s make sure that the program is getting the correct value from the user by adding some debug code to function getUserInput:

And the output:

main() called
getUserInput() called
Enter a number: 4
getUserInput::x = 4
main::x = 3
getUserInput() called
Enter a number: 3
getUserInput::x = 3
main::y = 2
add() called (x=3, y=2)
main::z = 5
printResult() called (z=5)
The answer is: 5

With this additional line of debugging, we can see that the user input is received correctly into getUserInput’s variable x. And yet somehow main’s variable x is getting the wrong value. The problem must be between those two points. The only culprit left is the return value from function getUserInput. Let’s look at that line more closely.

Hmmm, that’s odd. What’s that -- symbol before x? We haven’t covered that yet in these tutorials, so don’t worry if you don’t know what it means. But even without knowing what it means, through your debugging efforts, you can be reasonably sure that this particular line is at fault -- and thus, it’s likely this -- symbol is causing the problem.

Since we really want getUserInput to return just the value of x, let’s remove the -- and see what happens:

And now the output:

main() called
getUserInput() called
Enter a number: 4
getUserInput::x = 4
main::x = 4
getUserInput() called
Enter a number: 3
getUserInput::x = 3
main::y = 3
add() called (x=4, y=3)
main::z = 7
printResult() called (z=7)
The answer is: 7

The program is now working correctly. Even without understanding what -- was doing, we were able to identify the specific line of code causing the issue, and then fix the issue.

Why using printing statements to debug isn’t great

While adding debug statements to programs for diagnostic purposes is a common rudimentary technique, and a functional one (especially when a debugger is not available for some reason), it’s not that great for a number of reasons:

  1. Debug statements clutter your code.
  2. Debug statements clutter the output of your program.
  3. Debug statements must be removed after you’re done with them, which makes them non-reusable.
  4. Debug statements require modification of your code to both add and to remove, which can introduce new bugs.

We can do better. We’ll explore how in future lessons.


3.5 -- More debugging tactics
Index
3.3 -- A strategy for debugging

29 comments to 3.4 — Basic debugging tactics

  • As useful as std::cerr is, it makes the codes very messing in my opinion but I can see how useful it is to find errors.

  • Jim177

    Am I understanding this correctly?  With each calling to getUserInput with these statements.
    int x{ getUserInput() };
    int y{ getUserInput() };.
    In the function: int x{} value in the function, just gets copied into the x & y values.
    Is That Right?  Partial Code follows.
      
    int getUserInput()
    {
    std::cerr << "getUserInput() called\n";
        std::cout << "Enter a number: ";
        int x{};
        std::cin >> x;
    std::cerr << "getUserInput::x = " << x << '\n';
        return x; // removed -- before x
    }

    int main()
    {
    std::cerr << "main() called\n";
        int x{ getUserInput() };
    std::cerr << "main::x = " << x << '\n';
        int y{ getUserInput() };
    std::cerr << "main::y = " << y << '\n';

        int z{ add(x, y) };
    std::cerr << "main::z = " << z << '\n';
        printResult(z);

  • Alessandro

    Line five of the code

    should't be just single quote for the last round bracket?

    • Michael

      Either option will work but for consistency sake you should probably stick with the double quotes for any string literal that is not the newline character (\n)

      • Michael

        I stand corrected, the below rule is present in section 4.11 on Chars:

        "Always put stand-alone chars in single quotes (e.g. ‘t’ or ‘\n’, not “t” or “\n”). This helps the compiler optimize more effectively."

    • nascardriver

      Thanks for pointing this out, I updated it to

  • Microsoft Visual Studio isn't letting me even attempt to process some of these examples, it just skips right to failed without giving the expected incorrect lines. Particularly in problem 3, whether the figure is 5 or replaced by the correct variable y, the process fails with the same failed status, even while the IDE itself says no issues directly found. I will post the copy/pasted code below, as well as the error I am getting.

    1>------ Build started: Project: HelloWorld, Configuration: Debug Win32 ------
    1>HelloWorld.cpp
    1>HelloWorld.obj : error LNK2005: "int __cdecl add(int,int)" (?add@@YAHHH@Z) already defined in Add.obj
    1>C:\Users\sholi\source\repos\HelloWorld\Debug\HelloWorld.exe : fatal error LNK1169: one or more multiply defined symbols found
    1>Done building project "HelloWorld.vcxproj" -- FAILED.
    ========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

    • You can delete my question, I figured out what the issue was. I had an Add.cpp file attached to the project from a previous lesson and it was attempting to define add(x, y) function twice, and thus hitting an error.

  • Crouching_Liger

    Well I'd like to lead with thank you so very much for creating this site. It's been super helpful to me in learning the language for the first time. I tried to use it a few years ago and got as far as to design my first one off calculator but I wasn't able to continue much further past that because other studies started demanding priority and C++ wasn't an official class I was taking. Upon being able to return to this endeavor, I'm happy to rediscover that you've all managed to create a learning resource that not only breaks the language down and explains it in "layman's terms' but even goes into do's, don'ts, could but shouldn'ts, tips and tricks, etc. I've even recommended the website to friends who were struggling in their coding class who were able to use it to clear up their confusions and ace the course. You've all done such a great service and managed to even make the whole experience free right out of the gate. I just wanted to go ahead say all that before actually getting to my main question.

    So I'm actually curious as to why the "bug" in tactic #2 isn't recognized as a syntax error. I thought that it probably doesn't actually call the function and therefore receives nothing so std::cout ends up using some random memory it can find but when I tried recreating the problem by leaving it blank or adding a different identifier for a non-existent code, it gives me a syntax error. Is the issue that the parenthesis in the caller is what allows the value "4" to be stored and recognized by main() so that when the parenthesis is missing, main() calls "getValue", receives 4, has nowhere to put 4 so drops it, then uses whatever memory it can find? From my understanding so far, it feels like the compiler would see the lack of parenthesis as a syntax error and ask you to fix yet it doesn't. I just don't understand why the compiler lets it happen at all.

    • nascardriver

      Hi!

      I'm glad you're enjoying it :)

      Without the parentheses, the function never gets called. When a function name is used without parentheses, it is a pointer to that function (ie. the address of the function in memory). Function pointers have legitimate uses which we cover later. The compiler can't error out, because the code is valid and might just be what you want. Though, some compilers might be smart enough to detect that you probably intended to call the function and forgot the parentheses. In that case, they can issue a warning.

  • aspiring1

    Your Tip::When printing information for debugging purposes, use std::cerr instead of std::cout. One reason for this is that std::cout may be buffered, which means there may be a pause between when you ask std::cout to output information and when it actually does. If you output using std::cout and then your program crashes immediately afterward, std::cout may or may not have actually output yet. This can mislead you about where the issue is. On the other hand, std::cerr is unbuffered, which means anything you send to it will output immediately. This helps ensure all debug output appears as soon as possible (at the cost of some performance, which we usually don’t care about when debugging).

    Hi Alex, Nascardriver - I didn't get what it means that std::cout might be buffered and why it will slow down the output? Also, wouldn't std::cerr have the same problem of getting buffered, when we are using multiple lines of printing debugging code such as :

    as above we are using std::cerr twice, also didn't get the part about performance cost, how will using std::cerr (debugging statements lead to performance costs, other than their own running time?

    • nascardriver

      > std::cout might be buffered
      It doesn't print your text immediately. It stores it in memory, and prints in at some point in the future. If your program crashes before the text gets printed, you won't see it.

      > why it will slow down the output?
      Printing immediately (eg. with std::cerr) is slower than printing later. When the text gets printed later, more text will have accumulated in memory so the computer doesn't need to print as often.

      > wouldn't std::cerr have the same problem of getting buffered
      No, `std::cerr` prints immediately. Before line 4 is reached, the text has been printed.

  • Rom

    I prefer printing text and values for variables in debugging because it's easier than using a debugger. To avoid the problems mentioned above in printing text, you just need to declare one variable throughout your source code which controls whether the program will output debug text or not.

    For example on top of your code, there is a variable definition bool DEBUG{ true };, and in other parts of your debugging code, you have an if statement to check if DEBUG is true. If it is, you output debugging text, else not. If you want to stop debugging, you just need to set the variable to false.

  • Carl

    Hi

    Thanks for the great tutorial.

    In tactic 3, the code won't compile at all with the compiler 'Warning and error' settings set as recommended in 0.11 (-Wsign-conversion-Werror). If -Werror is removed the code does compile but with the warning "unused variable 'y'".

    Might be worth pointing out.

    Using Code::Blocks 16.01 in Linux Mint 19.

    • nascardriver

      Thanks! I updated the example to print `x` and `y` in `main` to get rid of the "unused variable" warning. If you find any more issues, please point them out :-)

  • Chayim

    What does the -c- of "cerr" stand for?
    c = console
    c = c (language c)
    c = character
    c = class
    ??

  • Baris

    Hi all, I didnt understand something

    int add(int x, int y) has two parameters x and y if  I write int z{ add(x, 5) } internal add function assumes y=5 so in example the result 9 why there is an error. In the outer space y may be =3 but in add function it is assumed 5. I think the result is true. I am new bee in C++ but i couldnt understand the logic why there is an error. Thanks

  • BP

    Hi!
    I have a quick question about the main function, I don't know if this is the best lesson to ask the question but either way.

    Is the main function supposed to be very empty, let me show an example of my main function while I was trying to do something:

    Can this be done better/nicer with multiple functions?

    • Line 18-21 and 28-31 are identical, they can be merged. Line 15-16 and 25-26 can be merged by using the conditional operator. Doing so will reduce the size of @main.
      I can't think of a reasonable function that should be added.

  • Red Lightning

    "If you program is exhibiting erroneous behavior"

    Syntax error: your program not you program.

  • Dimbo1911

    Great tutorial so far, just one minor remark, you should use uniform initialization in getUserInput function when initializing x (int x to int x{ }) in both examples, as you said in lection 1.4, to keep up with the best practices (and so people who read these would develop it into a habit). Once again, thank you for the great tutorials and keep up the good work.

  • Smidge

    I found a minor mistake on the 2nd example at the first debugging process. You missed another "getUserInput() called" on the second time the function is called. Thanks again for this tutorial. I will try to keep my eyes peeled so I can at least contribute a bit on this wonderful tutorial.

    main() called
    getUserInput() called
    Enter a number: 4
    //getUserInput() called
    main::x = 3
    Enter a number: 3
    main::y = 2
    add() called (x=3, y=2)
    main::z = 5
    printResult() called (z=5)
    The answer is: 5

  • Vili Sinervä

    Just wanted to point out a mistake at the end of the tutorial. The third point of the last section should probably say that you have to remember to remove print statements, not that you have to remove to remove them. The same line appears at the start of the next tutorial. Thanks for keeping these quality tutorials up-to-date and constantly expanding their scope!

Leave a Comment

Put all code inside code tags: [code]your code here[/code]