Lecture 11: Errors and Debugging | CMSC 240 Software Systems Development - Fall 2023

Lecture 11: Errors and Debugging

Objective

Learn and gain experience with errors and debugging, employ try/catch for exception handling, and apply debugging techniques including cout tracing, rubber duck debugging, and debugger tools to effectively diagnose and resolve programming challenges.

Lecture Topics

Errors

When we write programs, errors are natural and unavoidable; the question is, how do we deal with them?

Avoiding, finding, and correcting errors is 95% or more of the effort for serious software development.” – Bjarne Stroustrup

Common Sources of Errors

Kinds of Errors

The C++ compilation and linking process
CompilerLinker

Checking Your Inputs

One way to reduce errors is to validate your inputs.
Before trying to use an input value, check that it meets your expectations/requirements.

For example:

inputs.cpp

int area(int length, int width)
{
	return length * width;
}

int main()
{
    // error: wrong number of arguments
    int result1 = area(7);	   	    

    // error: 1st argument has a wrong type
    int result2 = area("seven", 2);	

    // ok
    int result3 = area(7, 10);		

    // ok, but dangerous: 7.5 truncated to 7
    // the compiler will warn you if you use
    // the -Wconversion warning option
    int result4 = area(7.5, 10);	
    
    // ok, but this is a difficult case:
    // the types are correct,
    // but the values make no sense
    int result5 = area(10, -7); 	

    return 0;
}

Bad Function Arguments

What do we do in cases like this, where the types are correct but the values don’t make sense:

int result = area(10, -7); 

Alternatives:

caller.cpp

    // Caller validates the inputs
    if (l <= 0)
    {
        error("Non-positive length value.");
    }
    
    if (w <= 0) 
    {
        error("Non-positive width value.");
    }
    
    int result = area(l, w);

    cout << "Area == " << result << endl;

function.cpp

// Returns a negative value for bad input.
int area(int length, int width)     
{
    // Validate the inputs.
	if(length <= 0 || width <= 0)
    { 
        // Return an error value.
        return -1;
    }

	return length * width;
}

The caller has to be aware of these special return values.

int result = area(l, w);

// Check the result for the -1 error return value.
if (result < 0)
{
    error("Bad area computation.");
}

For some functions there isn’t a “bad value” to return (e.g., max())

status.cpp

// Error status indicator.
int error_number = 0;

// Will set error_number to 7 for bad input.
int area(int length, int width)     
{
    // Validate the inputs.
	if(length <= 0 || width <= 0)
    { 
        // Set the error status.
        error_number = 7;
    }

	return length * width;
}

The caller has to be aware of the error number values.

int result = area(l, w);

// Check error_number to see if an error occurred.
if (error_number == 7)
{
    error("Bad area computation.");
}

cout << "Area == " << result << endl;

exception1.cpp

// Create a new exception class
class InvalidAreaArgumentsException
{
    // If you don't explicitly define a constructor, 
    // the compiler provides a default constructor 
};


// Will throw an exception on bad input.
int area(int length, int width)     
{
    // Validate the inputs.
	if(length <= 0 || width <= 0)
    { 
        // Throw an exception.
        throw InvalidAreaArgumentsException{};
    }

	return length * width;
}

The caller has the option to catch the exception.

try 
{
    int result = area(l, w);
    cout << "Area == " << result << endl;
}
catch (InvalidAreaArgumentsException &ex)
{
    cout << "Bad argument to area()" << endl;
}

If you don’t catch an exception it gets passed on along the call stack.

exception2.cpp

// Will throw an exception on bad input.
int area(int length, int width)     
{
    // Validate the inputs.
	if(length <= 0 || width <= 0)
    { 
        // Throw an exception.
        throw InvalidAreaArgumentsException{};
    }

	return length * width;
}

// Calls the area function after reducing 
// the length and width by frame size.
int framedArea(int length, int width)
{
    int frameSize = 2;

    // Do not catch exception here.
    int result = area(length - frameSize, width - frameSize);

    return result;
}

You can create more detailed exceptions that will keep track of variables and have custom messages for reporting errors.

exception3.cpp

#include <iostream>
#include <string>
using namespace std;

// Create a new exception class
class InvalidAreaArgumentsException
{
public:
    InvalidAreaArgumentsException(int l, int w) 
    : length{l}, width{w} 
    { /* Empty constructor block. */ }

    std::string message() 
    {
        return "Bad argument to area(): length == " 
            + std::to_string(length) 
            + " width == "
            + std::to_string(width);
    }
private:
    int length;
    int width;
};

// Will throw an exception on bad input.
int area(int length, int width)     
{
    // Validate the inputs.
	if(length <= 0 || width <= 0)
    { 
        // Throw an exception.
        throw InvalidAreaArgumentsException{length, width};
    }

	return length * width;
}

int main()
{
    int l;
    int w;
    cout << "Enter values for length and width:" << endl;
    cin >> l >> w;
    
    try 
    {
        int result = area(l, w);
        cout << "Area == " << result << endl;
    }
    catch (InvalidAreaArgumentsException &ex)
    {
        cout << exception.message() << endl;
    }

    return 0;
}

You could also choose from a selection of pre-defined exception classes in the <stdexcept> library.

Reference: https://en.cppreference.com/w/cpp/error/exception

Exceptions

For validating the arguments to the area function it makes sense to throw the invalid_argument exception defined in <stdexcept>.

exception4.cpp

#include <iostream>
#include <stdexcept>
using namespace std;

// Will throw an exception on bad input.
int area(int length, int width)     
{
    // Validate the inputs.
	if(length <= 0 || width <= 0)
    { 
        // Throw an exception.
        throw invalid_argument{"Bad argument to area()"};
    }

	return length * width;
}

int main()
{
    int l;
    int w;
    cout << "Enter values for length and width:" << endl;
    cin >> l >> w;
    
    try 
    {
        int result = area(l, w);
        cout << "Area == " << result << endl;
    }
    catch (invalid_argument &ex)
    {
        cout << ex.what() << endl;
    }

    return 0;
}

Exception should be thrown that describe the error that occurs. You can catch multiple types of exceptions and handle each of them differently. In the code below, the invalid_argument exception is thrown when bad arguments are passed to the area function, and the overflow_error exception is thrown if the multiplication of length and width overflow the maximum size of an integer. The catch statements are listed one after another.
exceptions5.cpp

#include <iostream>
#include <stdexcept>
using namespace std;

// Will throw an exception on bad input.
int area(int length, int width)     
{
    // Validate the inputs.
	if(length <= 0 || width <= 0)
    { 
        // Throw an invalid argument exception.
        throw invalid_argument{"Bad argument to area()"};
    }

    int result = length * width;

    // Check for an overflow in the result.
    if (result / length != width)
    {
        // Throw an overflow error exception.
        throw overflow_error{"Overflow occurred in area()"};
    }

	return result;
}

int main()
{
    int l;
    int w;
    cout << "Enter values for length and width:" << endl;
    cin >> l >> w;
    
    try 
    {
        int result = area(l, w);
        cout << "Area == " << result << endl;
    }
    catch (invalid_argument &ex)
    {
        cout << ex.what() << endl;
    }
    catch (overflow_error &ex)
    {
        cout << ex.what() << endl;
    }

    return 0;
}

Exceptions in C++ inherit from a base class called exception.

Overflow Error Exception
Overflow Error Exception

Invalid Argument Exception
Invalid Argument Exception

You can use polymorphism to catch multiple exceptions of different types by catching the parent class exception.

exception6.cpp

try 
{
    int result = area(l, w);
    cout << "Area == " << result << endl;
}
catch (exception &ex)
{
    cout << ex.what() << endl;
}

You can also check the cpp reference to see if functions you are calling throw exceptions.

For example: the string to integer stoi function. See reference: https://en.cppreference.com/w/cpp/string/basic_string/stol

Exercise 1

Your task is to enhance the factorial.cpp program in the Error directory to handle potential errors gracefully. Here are the steps:

if (result > std::numeric_limits<unsigned long long>::max() / i) 
{
    throw std::overflow_error("Result will overflow.");
}

Remember: Proper error handling is essential for building robust and user-friendly software. Take this opportunity to practice identifying potential errors in code and handling them gracefully!


Debugging

A bug in computer programming refers to an error, flaw, failure, or fault in a computer program or system that produces an incorrect or unexpected result, or causes it to behave in unintended ways. The term “bug” is said to have been popularized by Admiral Grace Hopper in the 1940s when a moth was found inside a computer relay, causing a fault.

Bug

When you have written a program, it will have bugs.

Debugging: Stepping Through a Program

Debugging: Beginnings and Ends

Debugging: Be Guided By Output

“If you can’t see the bug, you’re looking in the wrong place”

Types of Debugging

Adding cout Statements

testarea.cpp

#define DEBUG_ON true

// Create a debug function to output only if debug is on.
void debug(string message)
{
    if (DEBUG_ON)
    {
        cerr << message << endl;
    }
}

Now you can use the debug function throughout your code without worrying about having to comment out or remove the cout statements. Just turn DEBUG_ON to false.

// Calls the area function after reducing 
// the length and width by frame size.
int framedArea(int length, int width)
{
    debug("Begin framedArea");

    int frameSize = 2;

    // Do not catch exception here.
    int result = area(length - frameSize, width - frameSize);

    debug("Return from framedArea with result == " + to_string(result));

    return result;
}

Since we used cerr in our debug function, another trick is to redirect the cerr standard error stream (2) to a log file.

To redirect cerr (which is the standard error stream, or file descriptor 2 in shell terms) to a log file in a bash shell, you can use the following syntax:

$ ./your_command 2> log.txt

For example

$ ./testarea 2> debug.txt

You can also redirect the standard output stream (1) to an output file.

$ ./testarea 2> debug.txt 1> output.txt

Rubber Duck Debugging

RubberDuckDebugging

Link on Wikipedia: https://en.wikipedia.org/wiki/Rubber_duck_debugging

Debugger

Setup

First install the C/C++ extension and extension pack from Microsoft.
VSCodeExtension

Next, open a C++ file that you want to debug and click the debugger icon and then click run and debug.
VSCodeDebugger

The debugger may ask you to select a compiler. Select g++
VSCodeGPP

If you are debugging a project that has multiple C++ files (.cpp) then you may have to open the file tasks.json in the .vscode folder and modify the line that says "${file}", and change it to "*.cpp",
TasksJSON

Exercise 2

The program in the Debug folder simulates a basic library system where books can be added, borrowed, returned, and listed. There’s a runtime error in this program that you will need to find using the VSCode debugger.