Debugging warmup


This exercise will add a few new debugger tricks to your repertoire that will come in handy for this assignment and into the future.

Debugging objects

The files ball.h and ball.cpp define a Ball class that models a ball bouncing in a window. The member variables for a ball include its x and y position, its velocity on both axes, and the window that the ball is located in.

First, look over the code to get acquainted. Each ball is created with an ID number that is used as the label when drawing the ball. The ID number helps identify which ball is which in the drawing.

There is one provided test for the Ball class. This test creates a Vector of many ball objects and runs an animation loop to repeatedly move and redraw each ball.

Run the program and select the tests for ball.cpp. A new window pops up and a swarm of numbered balls bounce around within the window. Cool! After about 20 seconds, the test completes and the window closes.

screenshot of balls in motion

The ball animation has a bug that sometimes causes balls to bounce incorrectly. In this exercise, you will use the debugger to track down the bug.

Run the program again under the debugger. While the balls bounce, arrange the graphics and the debugger windows on your screen so you can see both simultaneously. In the debugger window, set a breakpoint on the line in ball.cpp where pause is called at the bottom of the animation loop. The debugger stops at this breakpoint almost immediately.

Click the Continue button continue button icon to resume execution, and the program will complete another loop iteration and stop again at the breakpoint. Repeatedly click the Continue button and watch the graphics window as you do this. In each loop iteration, all of the balls move slightly.

Object member variables

When stopped at the breakpoint, look at the "Variables" pane in the upper-right of the debugger window. Click the triangle to expand the allBalls Vector to see its contents. If the vector contents display as <not accessible>, follow the instructions to configure your Qt debugger to properly display Stanford collection types.

Click the triangles to expand the vector elements at indexes [0] and [1]. Each element is one Ball object. The member variables for a Ball include its x/y position, velocity, and ID. As a convention, we chose to name our member variables with a leading underscore. Note that each Ball has its own copy of the fields. The _x and _y of allBalls[0] are distinct from the _x and _y of allBalls[1].

Click Continue a few more times, observing the updates to the member variables for allBalls[0] and allBalls[1] in the Variables pane and match those updates to the movement of those balls in the graphics window.

Answer this question in short_answer.txt:

Q1. How do the values of the member variables of allBalls[0] change from iteration to iteration? Specifically, what happens to the values of _id, _x, and _y?

The variable this

Click on the red dot for your previous breakpoint to delete it. Set a new breakpoint on the first line of the Ball::move method. Do this without stopping and restarting the debugger so that the program continues to execute until it hits the new breakpoint. The debugger will now stop on each ball move. When stopped at the breakpoint, look at the variables in the Variables pane. Because the program is stopped inside a C++ member function, there is a special variable called this that refers to the object currently executing the member function. When stopped inside the function Ball::move, this refers to the specific ball that is taking its turn to move. Expand this to see the member variables of the object. Use this ball's position and ID to locate the corresponding ball in the graphics window.

Click the Continue button to resume. The breakpoint is hit again on the next call to Ball::move. Note that this now refers to a different Ball object with its own member variables. Click Continue a few more times to see some other balls take their turns to move.

Control-click on the red dot for your breakpoint to bring up the pop-up menu and choose Disable breakpoint. This leaves the breakpoint in place but makes it inactive. It is marked with a dim red dot. Use Continue to resume execution, and the balls should freely animate without stopping.

Debugging in the context of randomness

When the program begins, each ball is created with a starting position and velocity; these values are chosen randomly. Quit and restart the program a few times to observe how the initial configuration varies from run to run. Debugging a program that makes random choices can be challenging as you may not be able to reproduce a particular scenario at will. Sometimes your best option is to just keep an eye out for an intermittent bug and opportunistically jump into debugging whenever you encounter it.

If you run the program a few times, you may notice that occasionally a ball or two gets "stuck" along the bottom or right edge of the window. This is the bug we want you to diagnose. Let's see if we can get this under the debugger to investigate further.

Quit and restart the program in Debug mode until you get a starting configuration where at least one ball is stuck and enough of the stuck ball is visible that you can see its ID number. (It may take a few tries.)

Control-click on the dim red dot for the disabled breakpoint in Ball::move and choose Enable breakpoint from the pop-up menu. With this breakpoint now active, it stops at every single move for every single ball. Since there are many balls and each moves many times per second, having to stop and continue on each move is very tedious. We would much rather narrow our focus to the ball in trouble. For example, in this case, we know the ID of the stuck ball. Ideally, we want to stop and single step through the turn of the stuck ball, but zip past all the others.

Conditional breakpoints

The debugger offers just the tool we need: a conditional breakpoint. A condition can be added to a breakpoint to tell the debugger to only stop the program at this breakpoint if a certain condition is met.

Control-click on the breakpoint's red dot and choose Edit breakpoint from the pop-up menu. Look for the text field labeled Condition. The condition is any C++ expression that is valid to be evaluated at the breakpoint location. It can refer to any local variables and parameters. Each time execution reaches the breakpoint location, the condition is re-evaluated, and the debugger stops only if the condition evaluates to true; otherwise, it behaves as if the breakpoint were inactive and just continues on.

screenshot of the conditional breakpoint popup menu, showing the line number and condition on which to stop at that line

Let's try it out! Add a condition on the breakpoint to only stop if _id is the ID number of the stuck ball in your configuration. The breakpoint in Ball::move will only stop when executing for the stuck ball. Each Continue should take the program straight through to this particular ball's next move.

(Instructor's Note: We have found the Qt debugger to be a little flaky when handling breakpoint conditions. Sometimes it stops even when it shouldn't, but continuing once or twice seems to straighten it out. Your patience and tolerance will be rewarded; conditional breakpoints can be a great help for debugging a complex situation. However, if you give it a few tries and Qt refuses to cooperate, just note that in your answers and move on.)

Focus on observing what's happening to the stuck ball on each move. Do a couple of rounds with Continue, noting how the values of its member variables are changing. Contrast what is happening for the stuck ball to what you observed earlier for a correctly moving ball.

Answer the following question in short_answer.txt:

Q2. What is the pattern to how the values of the member variables of the stuck ball change from iteration to iteration?

Changing variables in the debugger

You have a hunch that moving the stuck ball to a different position would cause it to fix itself. So far, we have used the Variables pane of the debugger only to look at the values of variables. Pro Tip: it is also possible to change a value. Double-click the value for the _x field of the stuck ball and change it to 0. Change its _y to zero also. These changes forcibly move the stuck ball to position (0, 0). The next time this ball is drawn, it will appear in the upper left corner of the graphics window, where you have teleported it.

Answer the following question in short_answer.txt

Q3. After forcing the stuck ball to position (0, 0), does the ball move normally from there or does it stay stuck?

Given the above detective work, do you see what caused the ball to become stuck? What fix is needed to avoid getting into this situation? You may optionally modify the code to fix the bug, though this is not required.

A conditional breakpoint can be a godsend when debugging a program that works correctly almost all the time and only rarely hits a bug. Rather than having to wade through many uninteresting/irrelevant steps, the conditional breakpoint lets you narrow in on exactly and only the situation that needs attention.

Debugging arrays and memory

The last task is to practice debugging on arrays and memory. Pointers can be a fairly lethal part of the C++ language, as there are many pitfalls you can fall into with harsh consequences. It will take your finest debugging skills to diagnose these woes, so let's do some practicing now.

Changing the display format

You've used the Variables pane to see the values of variables, as expanded variables to see the internal contents and even changed the value of a variable. Our next Pro Tip is how to view the contents of an array.

Throughout the assignment, you will be working with a small struct called DataPoint that is defined in the datapoint.h file. Its definition is shown below. It declares the type for the name and priority fields and packages those two fields together into a new struct type called DataPoint.

struct DataPoint {
    string name;
    int priority;
};

In the assignment, you will be work with arrays of DataPoint elements so let's practice how to use the debugger to examine and manipulate these arrays.

Open the warmup.cpp file and review the given code and the five provided test cases (four of which are currently commented out). The first test case demonstrates correct use of an array. Set a breakpoint within this test case on the statement after the loop has completed initializing the array elements. Run the program under the debugger and select the tests for warmup.cpp.

When the program stops at the breakpoint, note how the shoppingList variable is displayed in the Variables pane. A C++ array does not track its length, so the debugger does not know how many elements to display. Unless told otherwise, the the debugger assumes the array length is 1 and displays only the first value. To see additional elements beyond the first, you must tell the debugger how many items to display.

In the Variables pane, control-click on the variable shoppingList to bring up the pop-up menu. From the menu, select "Change Value Display Format" and in the submenu section labeled "Change Display for Object Named local.shoppingList", select "Array of 10 items". Be sure to choose the entry "Array of 10 items" that appears in the upper section of the popup menu. (The lower section repeats the same entries as the upper section; this lower entry is not the one you want).

screenshot of the different options for displaying arrays in the Qt Debugger

The available options for array length are 10, 100, 1000, and 10000. If your array contains 50 items, you must choose whether to display only the first 10 or display 100, where the first 50 are valid followed by an additional 50 garbage items.

Examine array memory

Once you have changed the display format for shoppingList to an Array of 10 items, the Variables pane will display a triangle next the variable, and if you click to expand, it will display ten DataPoint structs, indexed by position.

The code allocates memory to store six DataPoint structs. The loop initializes the first three elements. Examine the values of the elements at index [0] [1] and [2] to see that the contents are as expected.

The subsequent three elements are a valid part of the memory allocated for the array, but the contents of these elements have not been initialized. C++ has set the values according to what it normally does for that type of variable. In particular,

  • the strings have been set to the empty string
  • the integer values are effectively random

Use the debugger to view the contents at indexes [3] [4] and [5] and confirm these default values.

Because we told the debugger to display a total of 10 elements, it also shows slots for indexes [6] through [9]. These locations are outside the bounds of the allocated memory, but remember there is no such thing as bounds-checking. We told the debugger to display 10, so it does. What is being displayed is the contents of memory at those locations, despite being out of bounds. Unsurprisingly, these contents are even trashier than the valid but uninitialized entries. if you examine slots [6] [7] [8] and [9], you find all of the integer and string values are an unpredictable mix of empty, <not accessible>, and random garbage.

What you see in the debugger parallels what you would observe in an executing program. The value of a valid variable that has not been initialized is the default value for that variable's data type. If your code accesses an index beyond the valid range of the array, you retrieve garbage contents. There is no helpful "out of bounds" message to alert you to your mistake. The programmer herself must be vigilant!

Debugging memory mistakes

The warmup.cpp file contains test cases to demonstrate various errors in handling array/memory. Whereas ordinary errors may cause a program to compute the wrong answer, mis-draw a fractal, or get stuck in a infinite loop, memory errors tend to have more catastrophic consequences. We want you to observe how some of the commonplace memory errors present themselves, so you can begin to get a feel for how you might diagnose such bugs when you encounter them.

There four test cases that each demonstrate a particular error in handling array/memory:

  1. dereferencing a nullptr address
  2. accessing an index outside the allocated array bounds
  3. accessing memory after it has been deallocated
  4. deallocating the same memory twice

Study the code in each test case to understand why the code is in error.

These test cases are currently commented out and you should add them into the program one at a time to observe each in isolation. Pro tip: To quickly change whether a section of code is commented in or out, select the lines and use "Toggle Comment Section" under the Edit Menu, bound to the hot key Command-/.

Uncomment the first test case (dereference nullptr) and rebuild. Run the program without the debugger.

This particular error is fatal – the program immediately halts, closes all windows and disappears, leaving behind only a pitiful cry for help in the application output. Click the "Application Output" tab at the bottom of the Qt Creator window to read the crash report:

*** STANFORD C++ LIBRARY
*** The PQueue program has terminated unexpectedly (crashed)
*** A segmentation fault (SIGSEGV) occurred during program execution

    This error indicates your program attempted to dereference a pointer
    to an invalid memory address (possibly out of bounds, deallocated, nullptr, ...)

*** To get more information about a program crash,
*** run your program again under the debugger.

Whenever you encounter a crash, your next move should be to run that same code under the debugger.

When running under the debugger, a crashing program will halt at the critical moment without exiting. This will give you a chance to see where the code was executing at the time of the crash and to poke around and observe the program state. Run this test case under the debugger, and it will stop at the exact line of the bad dereference, allowing you to see that the value of shoppingList is 0x0. (nullptr is the memory address zero).

After you're done exploring a test case, comment it back out. Then uncomment the next test case and try that one.

For each test case, you want to observe what happens without the debugger and again with it. Some memory errors present loud and clear with a distinctive crash; others silently and insidiously do the wrong thing. The very same error can get different results in a different context or when executing on a different operating system. All of this makes debugging memory errors extra challenging.

Consider yourself a detective who is looking through the evidence for clues that help you diagnose the situation from its aftermath. Look in the Application Output tab to see if any message was printed. Read the stack trace in the debugger to learn which action led up to the crash. Review variables to look for null or wild addresses or data values that appear uninitialized or corrupted.

Answer the following question in short_answer.txt:

Q4. On your system, what is the observed consequence of these three memory errors:

  • access an index outside the allocated array bounds?
  • access memory after it has been deallocated?
  • deallocate same memory twice?

Note: there is no "right answer" to these questions as there is no single expected behavior for a given memory error. By trying the error, you will see one possible outcome, but the same code in a different program or running on a different computer could behave differently. The only predictable thing about memory errors is their unpredictability! In answering these questions, you are making observations of how the memory error presents on your system and learning what symptoms to look for should you need to diagnose a similar error in your own program.