When writing programs, it is almost inevitable that you will make mistakes. In this section, we will talk about the different kinds of errors that are made, and how they are commonly handled.
Errors fall into two categories: syntax and semantic errors.
Syntax errors
A syntax error occurs when you write a statement that is not valid according to the grammar of the C++ language. For example:
if 5 > 6 then write "not equal";
Although this statement is understandable by humans, it is not valid according to C++ syntax. The correct C++ statement would be:
if (5 > 6)
std::cout << "not equal";
Syntax errors are almost always caught by the compiler and are usually easy to fix. Consequently, we typically don’t worry about them too much.
Semantic errors
A semantic error occurs when a statement is syntactically valid, but does not do what the programmer intended. For example:
for (int nCount=0; nCount<=3; nCount++)
std::cout << nCount << " ";
The programmer may have intended this statement to print 0 1 2, but it actually prints 0 1 2 3.
Semantic errors are not caught by the compiler, and can have any number of effects: they may not show up at all, cause the program to produce the wrong output, cause erratic behavior, corrupt data, or cause the program to crash.
It is largely the semantic errors that we are concerned with.
Semantic errors can occur in a number of ways. One of the most common semantic errors is a logic error. A logic error occurs when the programmer incorrectly codes the logic of a statement. The above for statement example is a logic error. Here is another example:
if (x >= 5)
std::cout << "x is greater than 5";
What happens when x is exactly 5? The conditional expression evaluates to true, and the program prints “x is greater than 5″. Logic errors can be easy or hard to locate, depending on the nature of the problem.
Another common semantic error is the violated assumption. A violated assumption occurs when the programmer assumes that something will be either true or false, and it isn’t. For example:
char strHello[] = "Hello, world!"; std::cout << "Enter an index: "; int nIndex; std::cin >> nIndex; std::cout << "Letter #" << nIndex << " is " << strHello[nIndex] << std::endl;
See the potential problem here? The programmer has assumed that the user will enter a value between 0 and the length of “Hello, world!”. If the user enters a negative number, or a large number, the array index nIndex will be out of bounds. In this case, since we are just reading a value, the program will probably print a garbage letter. But in other cases, the program might corrupt other variables, the stack, or crash.
Defensive programming is a form of program design that involves trying to identify areas where assumptions may be violated, and writing code that detects and handles any violation of those assumptions so that the program reacts in a predictable way when those violations do occur.
Detecting assumption errors
As it turns out, we can catch almost all assumptions that need to be checked in one of three locations:
- When a function has been called, the user may have passed the function parameters that are semantically meaningless.
- When a function returns, the return value may indicate that an error has occured.
- When program receives input (either from the user, or a file), the input may not be in the correct format.
Consequently, the following rules should be used when programming defensively:
- At the top of each function, check to make sure any parameters have appropriate values.
- After a function is called, check it’s return value (if any), and any other error reporting mechanisms, to see if an error occured.
- Validate any user input to make sure it meets the expected formatting or range criteria.
Let’s take a look at examples of each of these.
Problem: When a function is called, the user may have passed the function parameters that are semantically meaningless.
void PrintString(char *strString)
{
std::cout << strString;
}
Can you identify the assumption that may be violated? The answer is that the user might pass in a NULL pointer instead of a valid C-style string. If that happens, the program will crash. Here’s the function again with code that checks to make sure the function parameter is non-NULL:
void PrintString(char *strString)
{
// Only print if strString is non-null
if (strString)
std::cout << strString;
}
Problem: When a function returns, the return value may indicate that an error has occured.
// Declare an array of 10 integers int *panData = new int[10]; panData[5] = 3;
Can you identify the assumption that may be violated? The answer is that operator new (which actually calls a function to do the allocation) could fail if the user runs out of memory. If that happens, panData will be set to NULL, and when we use the subscript operator on panData, the program will crash. Here’s a new version with error checking:
// Delcare an array of 10 integers
int *panData = new int[10];
// If something went wrong
if (!panData)
exit(2); // exit the program with error code 2
panData[5] = 3;
Problem: When program receives input (either from the user, or a file), the input may not be in the correct format. Here’s the sample program you saw previously:
char strHello[] = "Hello, world!"; std::cout << "Enter an index: "; int nIndex; std::cin >> nIndex; std::cout << "Letter #" << nIndex << " is " << strHello[nIndex] << std::endl;
And here’s the version that checks the user input to make sure it is valid:
char strHello[] = "Hello, world!";
int nIndex;
do
{
std::cout << "Enter an index: ";
std::cin >> nIndex;
} while (nIndex < 0 || nIndex >= strlen(strHello));
std::cout << "Letter #" << nIndex << " is " << strHello[nIndex] << std::endl;
Handling assumption errors
Now that you know where assumption errors typically occur, let’s finish up by talking about different ways to handle them when they do occur. There is no best way to handle an error — it really depends on the nature of the problem.
Here are some typical responses:
1) Quietly skip the code that depends on the assumption being valid:
void PrintString(char *strString)
{
// Only print if strString is non-null
if (strString)
std::cout << strString;
}
In the above example, if strString is null, we don’t print anything. We have skipped the code that depends on strString being non-null.
2) If we are in a function, return an error code back to the caller and let the caller deal with the problem.
int g_anArray[10]; // a global array of 10 characters
int GetArrayValue(int nIndex)
{
// use if statement to detect violated assumption
if (nIndex < 0 || nIndex > 9)
return -1; // return error code to caller
return g_anArray[nIndex];
}
In this case, the function returns -1 if the user passes in an invalid index.
3) If we want to terminate the program immediately, the exit function can be used to return an error code to the operating system:
int g_anArray[10]; // a global array of 10 characters
int GetArrayValue(int nIndex)
{
// use if statement to detect violated assumption
if (nIndex < 0 || nIndex > 9)
exit(2); // terminate program and return error number 2 to OS
return g_anArray[nIndex];
}
If the user enters an invalid index, this program will terminate immediately (with no error message) and pass error code 2 to the operating system.
4) If the user has entered invalid input, ask the user to enter the input again.
char strHello[] = "Hello, world!";
int nIndex;
do
{
std::cout << "Enter an index: ";
std::cin >> nIndex;
} while (nIndex < 0 || nIndex >= strlen(strHello));
std::cout << "Letter #" << nIndex << " is " << strHello[nIndex] << std::endl;
5) cerr is another mechanism that is meant specifically for printing error messages. cerr is an output stream (just like cout) that is also defined in iostream.h. Typically, cerr writes the error messages on the screen (just like cout), but it can also be individually redirected to a file.
void PrintString(char *strString)
{
// Only print if strString is non-null
if (strString)
std::cout << strString;
else
std::cerr << "PrintString received a null parameter";
}
6) If working in some kind of graphical environment (eg. MFC or SDL), it is common to pop up a message box with an error code and then terminate the program.
Assert
Using a conditional statement to detect a violated assumption, along with printing an error message and terminating the program is such a common response to problems that C++ provides a shortcut method for doing this. This shortcut is called an assert.
An assert statement is a preprocessor macro that evaluates a conditional expression. If the conditional expression is true, the assert statement does nothing. If the conditional expression evaluates to false, an error message is displayed and the program is terminated. This error message contains the conditional expression that failed, along with the name of the code file and the line number of the assert. This makes it very easy to tell not only what the problem was, but where in the code the problem occurred. This can help with debugging efforts immensely.
The assert functionality lives in the cassert header, and is often used both to check that the parameters passed to a function are valid, and to check that the return value of a function call is valid.
int g_anArray[10]; // a global array of 10 characters
#include <cassert> // for assert()
int GetArrayValue(int nIndex)
{
// we're asserting that nIndex is between 0 and 9
assert(nIndex >= 0 && nIndex <= 9); // this is line 7 in Test.cpp
return g_anArray[nIndex];
}
If the user calls GetValue(-3), the program prints the following message:
Assertion failed: nIndex >= 0 && nIndex <=9, file C:\VCProjects\Test.cpp, line 7
We strongly encourage you to use assert statements liberally throughout your code.
Exceptions
C++ provides one more method for detecting and handling errors known as exception handling. The basic idea is that when an error occurs, it is “thrown”. If the current function does not “catch” the error, the caller of the function has a chance to catch the error. If the caller does not catch the error, the caller’s caller has a chance to catch the error. The error progressively moves up the stack until it is either caught and handled, or until main() fails to handle the error. If nobody handles the error, the program typically terminates with an exception error.
Exception handling is an advanced C++ topic, and we cover it in much detail in chapter 15 of this tutorial.
7.13 — Command line arguments
|
Index
|
7.11 — Namespaces
|
If you keep running into errors as you build your site, you might have to switch to cheap web hosting until you fix the kinks in your coding. You can find a dedicated hosting platform to host your site for less. You can even try to find free website hosting!
7.13 — Command line arguments
Index
7.11 — Namespaces
ok
Again, very clear and concise.
Two small typos:
“Defensive programming is a form of program design that…”
“// Delcare an array of 10 integers”
Thanks!
[ No, thank you! -Alex ]
char strHello[] = "Hello, world!"; int nIndex; do { std::cout <> nIndex; } while (nIndex = strlen(strHello)); std::cout << "Letter #" << nIndex << " is " << strHello[nIndex] << std::endl;In this Example, Shouldn’t it be
no arjun! ur while is just for to check validity of positive numbers within the bound of lenght of declared string, but other one was also to check the either user has also entered a negative number.
On Visual Studio 2008 Professional Edition, when an error is created, it states all the information in the Error List including which file and which line in that file the error occured. Therefore, do you need to use the assert() function when you are using VS 08 Pro?
To answer that question Michel, it’s good practice to put your own error handling in where ever possible.
The error handling can be used to put the errors into terms that the end user can understand, not just the programmer.
Error handling is not just good for debugging code, it’s also good to help point out errors in live programs being used by a client or user.
Alex THANK YOU FOR THIS ARTICLE!! I just sent it to a friend of mine who needed to see this, shall we say. ;)
// Delcare an array of 10 integers int *panData = new int[10]; // If something went wrong if (!panData) exit(2); // exit the program with error code 2 panData[5] = 3;This example is extremely outdated (1993 or so).
Nowadays, new operators through a bad_alloc exception if it can’t allocate the memory. So, it’s not necessary to check if panData==0 but exception handling should be implemented somewhere in your code. If you don’t catch the error, and you don’t re-define the unexpected() or terminate() functions, your program will terminate anyways making the safety/correctness of the statement: panData[5]=3; irrelevant.
You said: “cerr is an output stream (just like cout) that is also defined in iostream.h”. It is iostream or iostream.h?
The actual file is obviously iostream.h, but when you include it it is
You might wanna re-read the header file chapter.
If the user calls GetValue(-3), the program prints the following message:
Should be GetArrayValue