Programmers may lack experience with exceptions or got their experience with exceptions from languages (such as Java) where resource management and exception handling are not well integrated.
What’s the problem?
- If your software doesn’t check for and deal with errors, your program may be unreliable. Customers would be consider this to be poor quality software.
- Many real pieces of software should not be halted abnormally, and not just those that control medical devices or rockets.
- If exceptions are not used or handled properly, the process is abnormally terminated. When
std::abort()
is called, or if the implementation does not unwind the stack prior to callingstd::terminate()
, destructors for objects may not be called and external resources can be left in an indeterminate state. Abnormal process termination is the typical vector for denial-of-service (DOS) attacks.
What are C++ Exceptions?
C++ has a built-in notification system called exception handling. When a function detects an error, it can signal a condition (throw an exception) to some other part of the program whose job is to deal with that error.
Using the C++ exception handling system can make handling software errors simpler, more reliable, and more readable.
In modern C++, the preferred way to report and handle runtime errors is to use exceptions. This is especially true when the stack might contain several function calls between the function that detects the error, and the function that has the context to handle the error. Exceptions provide a formal, well-defined way for code that detects errors to pass the information up the call stack.
C++ exceptions : try, throw and catch
Exception handling in C++ consist of three keywords: try
, throw
and catch
:
The try
statement allows you to define a block of code to be tested for errors while it is being executed.
The throw
keyword throws an exception when a problem is detected, which lets you create a custom error.
The catch
statement allows you to define a block of code to be executed, if an error occurs in the associated try block.
The following simplified exception example shows the basic syntax for throwing and catching exceptions in C++:
#include <iostream>
#include <stdexcept> // To use runtime_error
// Defining function division
float division(float n, float d)
{
// If denominator is Zero
// throw runtime_error
if (d == 0) {
throw std::runtime_error("Math error: Attempted to divide by Zero\n");
}
// Otherwise return the result of division
return (n / d);
}
int main()
{
float numerator{ 2.3 }, denominator{ 0 }, result;
// try block calls the Division function
try {
result = division(numerator, denominator);
// this will not print in this example
std::cout << "The quotient is "
<< result << std::endl;
}
// catch block catches exception thrown
// by the Division function
catch (std::runtime_error& e) {
// prints that an exception has occurred
// calls the what function
// using runtime_error object
std::cout << "Exception occurred" << std::endl
<< e.what();
}
}
In the try
block, if an exception is thrown it will be caught by the first associated catch
block whose type matches that of the exception. In other words, execution jumps from the throw
statement to the catch
statement. If no usable catch block is found, std::terminate
is invoked and the program exits. In C++, any type may be thrown; however, we recommend that you throw a type that inherits directly or indirectly from std::exception
.
Standard Exceptions in C++
Exceptions are defined in the header files:
-
<stdexcept>
: Defines a set of standard exceptions that both the library and programs can use to report common errors. -
<exception>
: Defines the base class (i.e.,std::exception
) for all exceptions thrown by the elements of the standard library, along with several types and utilities to assist handling exceptions.
If you want to define your own exception class you can do so by inheriting from std::exception
.
All exceptions thrown by components of the C++ Standard library throw exceptions derived from the exception
class.
- std::exception - Parent class of all the standard C++ exceptions.
- logic_error - Exception happens in the internal logical of a program.
- domain_error - Exception due to use of invalid domain.
- future_error - Exception object that is thrown on failure by the functions in the thread library.
- invalid_argument - Exception due to invalid argument.
- out_of_range - Exception due to out of range i.e. size requirement exceeds allocation.
- length_error - Exception reports errors that result from attempts to exceed implementation defined length limits for some object.
- runtime_error - Exception happens during runtime.
- range_error - Exception due to range errors in internal computations.
- overflow_error - Exception due to arithmetic overflow errors.
- underflow_error - Exception due to arithmetic underflow errors
- regex_error - Exception object thrown to report errors in the regular expressions library
- system_error - Exceptions to report conditions originating during runtime from the operating system or other low-level application program interfaces which have an associated error_code.
- ios_base::failure - Embedded class inherits from
system_error
and serves as the base class for the exceptions thrown by the elements of the standard input/output library. - filesystem::filesystem_error - Defines an exception object that is thrown on failure by the throwing overloads of the functions in the filesystem library.
- ios_base::failure - Embedded class inherits from
- logic_error - Exception happens in the internal logical of a program.
- bad_alloc - Exception happens when memory allocation with
new()
fails. - bad_cast - Exception happens when dynamic cast fails.
- bad_exception - Exception is specially designed to be listed in the dynamic-exception-specifier.
- bad_function_call - Exception type thrown by empty function objects when their functional call is invoked.
- bad_typeid - Exception thrown by typeid.
- bad_weak_pt - Exception thrown by
shared_ptr
’s constructor when constructed with an expiredweak_ptr
.
Exceptions are for exceptional code
Exceptions should be used only to indicate exceptional conditions; they should not be used for ordinary control flow purposes. Catching a generic object is likely to catch unexpected errors.
Exceptions do not need to be highly optimized, as they should only be thrown in exceptional circumstances. Throwing and catching exceptions unnecessarily and frequently has worse performance than handling errors with another mechanism.
Program errors are often divided into two categories: Logic errors that are caused by programming mistakes, for example, an “index out of range” error.
#include <iostream>
#include <stdexcept>
#include <vector>
int main(void) {
std::vector<int> mem = { 1,2,3 };
try {
mem.at(5) = 5; /* vector::at throws an
Out of Range error: vector::_M_range_check: __n (which is 5) >= this->size() (which is 3) */
}
catch (const std::out_of_range& oor) {
std::cerr << "Out of Range error: " << oor.what() << '\n';
}
return 0;
}
And, other runtime errors that are beyond the control of the programmer, for example, a “network service unavailable” error. In C-style programming, error reporting is managed either by returning a value that represents an error code or a status code for a particular function, or by setting a global variable that the caller may optionally retrieve after every function call to see whether any errors were reported. Setting a global variable for errors doesn’t work well unless you test it immediately, because some other function might have re-set it.
When programmers use errno
and if
statements, their error handling and normal code are closely intertwined. The code gets messy and it becomes hard to ensure that all errors have been dealt with (“spaghetti code”).
ofstream os("filename.txt"); // open a file
if(os.bad()) { /* handle error */ }
For example, in the case of ofstream
, your output simply disappears if you forget to check that an open operation succeeded.
It’s up to the caller to recognize the code and respond to it appropriately. If the caller doesn’t explicitly handle the error code, the program might crash without warning. Or, it might continue to execute using bad data and produce incorrect results.
Exceptions are preferred in modern C++ for the following reasons:
-
Exceptions force the calling code to recognize an error condition and handle it. Unhandled exceptions stop program execution.
-
An exception jumps to the point in the call stack that can handle the error. Intermediate functions can let the exception propagate. They don’t have to coordinate with other layers.
-
The exception stack-unwinding mechanism destroys all objects in scope after an exception is thrown, according to well-defined rules.
-
Exceptions enable a clean separation between the code that detects the error and the code that handles the error.
Another C++ exception example:
// invalid_argument example
#include <iostream>
#include <stdexcept>
#include <bitset>
#include <string>
int main () {
try {
// bitset constructor throws an invalid_argument if initialized
// with a string containing characters other than 0 and 1
std::bitset<5> your_bitset (std::string("01234"));
}
catch (const std::invalid_argument& ia) {
std::cerr << "Invalid argument: " << ia.what() << '\n';
}
return 0;
}
In the above example, the exception type, invalid_argument
, is defined in the standard library in the <stdexcept>
header file. C++ doesn’t provide or require a finally
block (like Java) to make sure all resources are released if an exception is thrown. The resource acquisition is initialization (RAII) idiom, which uses smart pointers, provides the required functionality for resource cleanup.
How C++ exceptions improve software quality
By eliminating one of the reasons for if
statements.
There are many reasons why branching is bad, including cache misses, more required testing, maintenance, and bugs.
Although returning an error code is sometimes the most appropriate error handling technique, there are some bad side effects to adding unnecessary if
statements:
- Degrades quality: It is well known that conditional statements are about ten times more likely to contain errors than any other kind of statement. So all other things being equal, if you can eliminate conditionals / conditional statements from your code, you will likely have more robust code.
- Slows down time-to-market: Since conditional statements are branch points which are related to the number of test cases that are needed for white-box testing, unnecessary conditional statements increase the amount of time that needs to be devoted to testing. Basically, if you don’t exercise every branch point, there will be instructions in your code that will never have been executed under test conditions until they are seen by your end users/customers.
- Increased development cost: Bug finding, bug fixing, and testing effort are all increased by unnecessary control flow complexity.
Basic C++ error handling guidelines
Effective and robust error handling can be challenging in any programming language. Although exceptions provide several features that support good error handling, they can’t do all the work for you. To realize the benefits of the exception mechanism, keep exceptions in mind as you design your code.
Top 11 C++ error handling guidelines
-
Do not abruptly terminate the program. The
std::abort(
),std::quick_exit()
, andstd::_Exit()
functions are used to terminate the program immediately. They do so without calling exit handlers registered withstd::atexit()
and without executing destructors for objects with automatic, thread, or static storage duration. -
Handle all exceptions. When an exception is thrown, control is transferred to the nearest handler with a type that matches the type of the exception thrown. If no matching handler is found within the handlers for a try block in which the exception is thrown, the search for a matching handler continues to look for handlers in the surrounding try blocks of the same thread.
If no matching exception handler is found, the function
std::terminate()
is called; whether or not the stack is unwound before this call tostd::terminate()
is implementation-defined.The default terminate handler called by std::terminate() calls
std::abort()
, which abnormally terminates the process. Whenstd::abort()
is called, or if the implementation does not unwind the stack prior to callingstd::terminate()
, destructors for objects may not be called and external resources can be left in an indeterminate state. Abnormal process termination is the typical vector for denial-of-service (DOS) attacks. -
Guarantee exception safety. Proper handling of errors and exceptional events is essential for the continued correct operation of software. The preferred mechanism for reporting errors in a C++ program is exceptions rather than error codes. A number of core language facilities, including dynamic_cast, operator
new()
, andtypeid
, report failures by throwing exceptions. Also, the C++ standard library often uses exceptions to report several different kinds of failures. Few C++ programs manage to avoid using some of these facilities. For this reason, the majority of C++ programs must be prepared for exceptions to occur and must handle each appropriately. -
Honor exception specifications. If a function declared with a dynamic-exception-specification throws an exception of a type that would not match the exception-specification, the function
std::unexpected()
is called. The behavior of this function can be overridden but, by default, causes an exception ofstd::bad_exception
to be thrown. Unlessstd::bad_exception
is listed in the exception-specification,std::terminate()
will be called.Similarly, if a function declared with a noexcept-specification throws an exception of a type that would cause the noexcept-specification to evaluate to false, the function
std::terminate()
will be called. -
Throw exceptions by value, catch them by reference. Don’t catch what you can’t handle. Avoid throwing a pointer, because if you throw a pointer, you need to deal with memory management issues: You can’t throw a pointer to a stack-allocated value because the stack will be unwound before the pointer reaches the call site.
-
Use asserts to check for errors that should never occur. Use exceptions to check for errors that might occur, for example, errors in input validation on parameters of public functions.
-
Use exceptions when the code that handles the error is separated from the code that detects the error by one or more intervening function calls. Consider using error codes instead in performance-critical loops, when code that handles the error is tightly coupled to the code that detects it.
-
For every function that might throw or propagate an exception, provide one of the three exception guarantees: the strong guarantee, the basic guarantee, or the nothrow (noexcept) guarantee.
-
Don’t use exception specifications, which were deprecated in C++11. Exception specifications were introduced in C++ as a way to specify the exceptions that a function might throw. However, exception specifications proved problematic in practice, and were deprecated in the C++11 draft standard. We recommend that you don’t use
throw
exception specifications except forthrow()
, which indicates that the function allows no exceptions to escape. -
Use standard library exception types when they apply. Derive custom exception types from the
exception
Class hierarchy. -
Don’t allow exceptions to escape from destructors or memory-deallocation functions. Under certain circumstances, terminating a destructor, operator
delete
, or operatordelete[]
by throwing an exception can trigger undefined behavior. Object destructors are likely to be called during stack unwinding as a result of an exception being thrown. If the destructor itself throws an exception, having been called as the result of an exception being thrown, then the functionstd::terminate()
is called with the default effect of callingstd::abort()
.
C++ exceptions and performance
The exception mechanism in C++ has a minimal performance cost if no exception is thrown. If an exception is thrown, the cost of the stack traversal and unwinding is about the same as the cost of a function call. Additional data structures are required to track the call stack after a try
block is entered, and additional instructions are required to unwind the stack if an exception is thrown. However, in most cases, the cost in performance and memory isn’t significant. The adverse effect of exceptions on performance is likely to be significant only on memory-constrained systems. Or, in performance-critical loops, where an error is likely to occur regularly and there’s tight coupling between the code to handle it and the code that reports it. In any case, it’s impossible to know the actual cost of exceptions without profiling and measuring. Even in those rare cases when the cost is significant, you can weigh it against the increased correctness, easier maintainability, and other advantages that are provided by a well-designed exception policy.
C++ exceptions versus assertions
Exceptions report errors found at run time. If an error can be found at compile time, it is usually preferable to do so. That’s what much of the type system and the facilities for specifying the interfaces to user-defined types are for. However, you can also perform simple checks on other properties that are known at compile time and report failures as compiler error messages by using static assertions.
static_assert( constant_expression, string_literal );
static_assert()
uses an integral constant expression that can be converted to a Boolean. If the evaluated expression is zero (false), the string-literal parameter is displayed and the compilation fails with an error. If the expression is nonzero (true), the static_assert
declaration has no effect.
#include <type_traits>
#include <iostream>
struct YourStruct{
int num{};
int num2{};
char c[5];
};
template <class T, int vec_size>
class Vector {
// Compile time assertion to check
// the size of the vector
static_assert(vec_size > 3, "Vector size is too small!");
T m_values[vec_size];
};
int main()
{
// These will fail
static_assert(sizeof(YourStruct) <= 12, "Struct size is greater than 12.");
YourStruct ys;
std::cout << "Size of ys is "<< sizeof(ys) ;// Size of ys is 16
Vector<size_t, 2 > vec_two;
return 0;
}
The ‘constant_expression’ parameter represents a software assertion (a condition that you expect to be true at a particular point in your program) that needs to be checked during compile time. If the condition is true, the static_assert declaration has no effect. If the condition is false, the assertion fails, and the compiler displays the message in string_literal parameter and the compilation fails with an error.
For example:
static_assert(sizeof(void *) == 8, "64-bit code generation is supported.");
Exceptions and asserts are two distinct mechanisms for detecting errors in a program. Use assert
statements to test for conditions during development that should never be true if all your code is correct. There’s no point in handling such an error by using an exception, because the error indicates that something in the code has to be fixed. It doesn’t represent a condition that the program has to recover from at run time. An assert
stops execution at the statement so that you can inspect the program state in the debugger. An exception continues execution from the first appropriate catch handler. Use exceptions to check error conditions that might occur at run time even if your code is correct, for example, “file not found” or “out of memory.” Exceptions can handle these conditions, even if the recovery just outputs a message to a log and ends the program. Always check arguments to public functions by using exceptions. Even if your function is error-free, you might not have complete control over arguments that a user might pass to it.
Exception specifications and noexcept
Exception specifications were introduced in C++ as a way to specify the exceptions that a function might throw. However, exception specifications proved problematic in practice, and are deprecated in the C++11 draft standard. We recommend that you don’t use throw
exception specifications except for throw()
, which indicates that the function allows no exceptions to escape. The noexcept
specifier is introduced in C++11 as the preferred alternative to throw()
.
Conclusion
By being aware of C++ exceptions and designing your software to use them effectively by following some simple rules, your code can be cleaner, more reliable and safer.
References:
- Exception safety: Concepts and techniques
- Modern C++ best practices for exceptions and error handling
- SEI CERT C++ Coding Standards : Rules
Learn more about the benefits of programming your applications with modern C++
Related services: C++ Software Development