5 Awesome Visual Studio Debugger Features

So you know all the basic debugger features: breakpoints, stepping through the code, viewing the values of variables. But there's a lot more your debugger can do to make it quicker to step through code, easier to test alternate solutions, and even avoid rerunning the program.

Here, you'll learn about some of Visual Studio's more advanced debugger features, such as stepping out of a function, taking advantage of the autos window, using run to the cursor to set temporary breakpoints, modifying a variable's value to test potential fixes, and using Set Next Statement to turn back the clock and re-execute code.

Sample (Buggy) Code

Each of the following examples use this code, which contains several bugs:
#include <vector>
#include <iostream>

class Data
	Data ()
		_values.push_back( 1 );
		_values.push_back( 2 );
	int getSum()
		int total;
		for ( std::vector<int>::iterator itr = _values.begin(), end = _values.end(); itr != end; ++itr )
			total += *itr;
		return total;
	int getCount() { return _values.size(); }
	std::vector<int> _values;

double compute_average( int sum, int count )
	return sum / count;

int main()
	Data d;

	std::cout << compute_average( d.getSum(), d.getCount() ) << std::endl;

1. Step Out of the Current Function

So you've written a function call, like so:
int main()
	Data d;

	std::cout << compute_average( d.getSum(), d.getCount() ) << std::endl;
And the compute_average function is not returning the correct value. If you were to use a debugger, and you wanted to step into compute_average, you could, of course, put a breakpoint inside compute_average, but what if it were called from several places? Visual Studio has a very convenient feature of its debugger, that will allow you to step into compute_average very quickly.

Set a breakpoint on that line:

Once the debugger hits it, then step into the function. getSum and getCount will both be called before compute_average. Normally, you'd keep hitting next to get out of getSum and getCount, but with Visual Studio, you can quickly hit Shift-F11 to step out of the current function and return to the next call.

Once you step out of the current function, you're taken back to the original breakpoint. But now you can step in again, to go to the next function. Repeat until you've drilled down into the function you want.

When to Use This Trick

Obviously, there are times when it makes more sense to just set a breakpoint in your target function. But when doing so would require ignoring hundreds of calls to that function to find the one you want, it might not be worth it. Using step out provides a quick way of getting into a function without having to find the function and set a temporary break point (or use run to cursor).

2. Use the Auto Window to See Result of Functions

One of the most frustrating parts of debugging is when you have a function call whose result isn't stored anywhere, and you really want to know what it returned.

Sometimes, programmers write code like this, just to work around the issue:
int computed_avg;

computed_avg = compute_average( d.getSum(), d.getCount() );

std::cout << computed_avg << std::endl;
(Obviously, in this case, the program prints out the value, so all is not lost. This is rarely true in the general case.)

Fortunately, the autos window has good courtesy to display the result of a function evaluation:

When to Use this Trick

Whenever you want to see a return value! Note that you the autos window will eventually erase the return value as you execute code, so be sure to check your return value immediately.

Did you know...

Most return values are also stored in the EAX register--you can look in EAX to find the return value, if you need it.

3. Run to Cursor

Run to cursor is a great way of avoiding the step-step-step-step-step-oh-no-I-stepped-over-it problem, where you know where you want to be, getting to it requires stepping multiple times, and you get impatient.

In effect, run-to-cursor is just a shortcut for setting a breakpoint that is immediately cleared once it's hit; nevertheless, it's a very convenient feature. It is most useful when you have a piece of code that is called frequently from different places, and you only care about one code path. Then you can place a breakpoint at the start of that path and use run-to-cursor to get to the point you really care about.

Going back to our sample program, we can use run-to-cursor rather than the step-out trick, to get into compute_average. (Of course, we could just put a breakpoint in compute_average; for the purpose of making this example sensible, please imagine 15-20 calls to compute_average that all work correctly, taking place before the broken call.)

When to Use this Trick

Any time you want a throwaway breakpoint or want to avoid single-stepping through a lot of code. Be careful, though, that you run to a part of the code that will actually be executed. Watch out for early returns from a function within a loop, for instance.

4. Modify Any Variable

Now that we're in the compute_average function, and we know that the value being returned is waayyy too big, we can check the arguments to the function. It turns out that sum is very large. We'll deal with that in a bit. First, let's test and make sure that the function works if we do get the right value.

Obviously, one way of doing this would be to pass in a new value. But Visual Studio conveniently makes it easy to change any value in memory. In fact, let's do that with the value of sum, and make sure that it returns the correct value.

All you need to do is click on in the Value column of the auto or watch window and change the value. (You can also do this when the variable's value pops up while hovering over a variable in the source code.)

and voila:

Continuing execution of the program demonstrates that, in fact, we still get the wrong answer--this time, it returns the value 1. This suggests that truncation is taking place due to integer division.

Before recompiling, we can add in a cast to solve this problem:
return (double) sum / count;

When to Use this Trick

This trick is powerful, and is particularly helpful when you have found one bug, but want to prove that the rest of your code will work. This is particularly handy when your debugging session requires a great deal of setup--for instance, you have to perform a lot of UI to get the right inputs or your code takes a long time to build. (An alternative would be to consider writing a unit test that demonstrates the bug and doesn't require so much setup.)

On the other hand, you can't rely on this trick when you are inside a loop or a function that is called frequently--it's just too much of a pain to have to manually set variables all the time.

5. Set Next Statement

Set Next Statement is a real power tool. If you're debugging, and you've accidentally (or not so accidentally) stepped past the point where something interesting happens, you can sometimes "unwind" execution. What this really means is that, maintaining the current state of the world, you can tell Visual Studio to go back and start executing from a previous instruction.

You can also use set next statement to jump over code that you believe is buggy, or to jump past a conditional test on an if statement to execute a path of code that you want to debug without having to make the condition true.

Setting the next statement can be dangerous and lead to instability if the stack or registers aren't in a valid state for the line being executed. And because it won't restore the state of the world, if your code depends on that state, changing the next statement might not be useful. On the other hand, if you want to make a call that you're pretty confident won't behave differently, it can be a great way of avoiding recreating a specific failure just because you accidentally stepped too far.

For instance, take the following debugging state, where you're about to return the value total from getSum:

If you had accidentally run too far, it might be very convenient to be able to simply say, ok, let's start that again:

and then you can go back through executing the loop, perhaps setting the value of total to 0, since you notice that it wasn't initialized, and then checking to see if the program gives the correct sum (in which case, you can be pretty confident that the lack of initialization was the problem).

When to Use this Trick

Many of the same considerations that apply to resetting variable values also apply here. However, you should also be aware that the further ahead or back in a function you set the next statement, the more likely the debugger will not be able to compensate and your program will crash. I usually use this trick to skip over very small chunks of code, or to go back only to a single previous function call.

subscribe to a feed Subscribe to Cprogramming.com

Related articles

Skip Stepping Into Functions with Visual Studio's NoStepInto Option.

Debugging with Visual Studio Part 1: Debugging Concepts

Debugging with Visual Studio Part 2: Setting up the Debugger

Debugging with Visual Studio Part 3: Using Breakpoints Effectively

Debugging with Visual Studio Part 4: Setting up Code for the Debugger

Debugging with Visual Studio Part 5: Using Trace and Log Messages

Debugging with Visual Studio Part 6: Remote Debugging