[C++] One article to understand the exception handling mechanism in C++

Exception handling mechanism in C++

1.1 What is an exception?

The program sometimes encounters a runtime error, which prevents the program from running normally. For example, a program might try to open a file that is not available, request too much memory, or encounter an intolerable value. Usually, programmers try to prevent such unexpected situations. C++ exceptions provide a powerful and flexible tool for handling this situation.

2.0 * x * y / (x + y);

For the above expression, if y is a negative value of x, the above formula will result in division by zero - an operation that is not allowed. Many modern compilers handle division by zero by generating a special floating-point value representing infinity, which cout displays as Inf, inf, INF, or similar; while other compilers may generate Program that crashes when dividing by zero. It's better to write code that runs in the same controlled way on all systems.

1.2 Call abort()

One way to deal with this kind of problem is to call the abort() function if one of the parameters is the negative value of the other. The prototype of the abort() function is located in the header file cstdlib (or stdlib.h), and its typical implementation is to send a message abnormal program termination (abnormal program termination) to the standard error stream (that is, the error stream used by cerr), and then terminate the program. It also returns an implementation-dependent value that tells the operating system (or the parent process, if the program was called by another program) that processing failed. Whether abort() flushes the file buffer (the area of ​​memory used to store data read or written to the file) is implementation-dependent. You can also use exit() if you prefer, which flushes the file buffer but does not display a message.

//error1.cpp -- using the abort() function
#include <iostream>
#include <cstdlib>
double hmean(double a, double b);
 
int main() {
    double x, y, z;
    std::cout << "Enter two numbers: ";
    while (std::cin >> x >> y) {
        z = hmean(x,y);
        std::cout << "Harmonic mean of " << x << " and " << y
            << " is " << z << std::endl;
        std::cout << "Enter next set of numbers <q to quit>: ";
    }
    std::cout << "Bye!\n";
    return 0;
}
 
double hmean(double a, double b) {
    if (a == -b) {
        std::cout << "untenable arguments to hmean()\n";
        std::abort();
    }
    return 2.0 * a * b / (a + b); 
}

Note that calling the abort( ) function in hmean( ) will directly terminate the program instead of returning to main( ) first. In general, the program abort messages displayed vary by compiler.

1.3 return error code

A more flexible approach than aborting is to use the return value of a function to indicate a problem.

For example, the get(void) member of the ostream class usually returns the ASCII code of the next input character, but when the end of file is reached, the special value EOF is returned.

For hmean(), this method does not work. Any numeric value is a valid return value, so there are no special values ​​that can be used to indicate a problem. In this case, use a pointer or reference parameter to return a value to the calling program, and use the function's return value to indicate success or failure. The istream family of overloaded >> operators uses a variation of this technique. Allows a program to take action other than terminating the program by notifying the calling program of success or failure. The following program listing is an example of this method. It redefines the return value of hmean() as bool, making the return value indicate success or failure. In addition, it adds a third parameter to the function to provide Answer.

//error2.cpp -- returning an error code
#include <iostream>
#include <cfloat>  // (or float.h) for DBL_MAX
bool hmean(double a, double b, double * ans);
 
int main() {
    double x, y, z;
    std::cout << "Enter two numbers: ";
    while (std::cin >> x >> y) {
        if (hmean(x,y,&z))
            std::cout << "Harmonic mean of " << x << " and " << y
                << " is " << z << std::endl;
        else
            std::cout << "One value should not be the negative "
                << "of the other - try again.\n";
        std::cout << "Enter next set of numbers <q to quit>: ";
    }
    std::cout << "Bye!\n";
    return 0;
}
 
bool hmean(double a, double b, double * ans) {
    if (a == -b) {
        *ans = DBL_MAX;
        return false;
    } else {
        *ans = 2.0 * a * b / (a + b);
        return true;
    }
}

1.4 Exception mechanism

Here's how to use the exception mechanism to handle errors:

A C++ exception is a response to an abnormal condition (such as division by zero) that occurs during the execution of the program. Exceptions provide a means of passing control from one part of a program to another. Exception handling has 3 components:

  • raise exception
  • Use handlers to catch exceptions
  • use try block

The throw statement is actually a jump, that is, it commands the program to jump to another statement. The throw keyword indicates that an exception is thrown, and the value immediately following it (such as a string or object) indicates the characteristics of the exception.

Programs catch exceptions using exception handlers, which are located in the program that handles the problem. The catch keyword means to catch exceptions. A handler begins with the catch keyword, followed by a type declaration in parentheses that specifies the type of exception to which the exception handler responds, and then a block of code enclosed in curly braces that specifies the action to take. The catch keyword and the exception type are used as labels to indicate that the program should jump to this location when an exception is raised. Exception handlers are also known as try catch blocks.

A try block identifies a block of code in which a specific exception may be activated, followed by one or more catch blocks. The try block is indicated by the keyword try, followed by a code block enclosed in curly braces, indicating that attention should be paid to the exceptions caused by these codes.

// error3.cpp -- using an exception
#include <iostream>
double hmean(double a, double b);
int main() {
    double x, y, z;
    std::cout << "Enter two numbers: ";
    while (std::cin >> x >> y) {
        try {
            z = hmean(x,y);
        } catch (const char * s) {// start of exception handler
            std::cout << s << std::endl;
            std::cout << "Enter a new pair of numbers: ";
            continue;
        }                       // end of handler
        std::cout << "Harmonic mean of " << x << " and " << y
            << " is " << z << std::endl;
        std::cout << "Enter next set of numbers <q to quit>: ";
    }
    std::cout << "Bye!\n";
    return 0;
}
 
double hmean(double a, double b) {
    if (a == -b)
        throw "bad hmean() arguments: a = -b not allowed";
    return 2.0 * a * b / (a + b); 
}

The try block looks like this:

try{
z = hmean(x,y);
}

If one of the statements causes an exception to be thrown, the following catch block will handle the exception. If the program calls hmean( ) outside the try block, the exception cannot be handled.

The code that throws the exception is similar to the following:

if (a == -b)
        throw "bad hmean() arguments: a = -b not allowed";

where the exception thrown is the string "bad hmean( )arguments: a = -b not allowed". The exception type can be a string (as in this example) or another C++ type; usually a class type.

Executing a throw statement is similar to executing a return statement in that it also terminates the execution of the function; but instead of returning control to the calling program, throw causes the program to back up the sequence of function calls until it finds the function containing the try block. In this example, throw returns program control to main( ). The program will look in main( ) for an exception handler (located after the try block) that matches the type of exception raised. The only catch block in the program takes a char* as an argument, so it matches throwing an exception.

The catch block point is similar to a function definition, but it is not a function definition. The keyword catch indicates that this is a handler, while char*s indicates that the handler matches a string exception. s is very similar to a function parameter definition in that matching triggers will be assigned to s. Additionally, the program executes the code enclosed in parentheses when an exception matches this handler.

After executing the statements in the try block, if no exception is raised, the program skips the catch block after the try block and directly executes the first statement after the handler.

Let's see what happens when we pass 10 and −10 to the hmean( ) function. The If statement causes hmean( ) to throw an exception. This will terminate the execution of hmean( ). As the program searches backwards, the hmean( ) function is called from the try block in main( ), so the program looks for a catch block that matches the exception type. The only catch block in the program takes a char* as an argument, so it matches throwing an exception. The program assigns the string "bad hmean( )arguments: a = -b not allowed" to the variable s, and then executes the code in the handler. The handler first prints s - the caught exception, then prints an instruction to ask the user to enter new data, and finally executes the continue statement, which instructs the program to skip the rest of the while loop and jump to the beginning. continue causes the program to jump to the beginning of the loop, which indicates that the handler statement is part of the loop, and the catch line is a label that directs the program flow.

insert image description here

What happens if a function throws an exception and there is no try block or matching handler. By default, the program will eventually call the abort() function, but this behavior can be modified. This issue will be discussed later.

1.5 Using objects as exception types

Typically, the function that throws an exception will be passed an object. One of the important advantages of this is that different exception types can be used to distinguish exceptions thrown by different functions under different circumstances. In addition, objects can carry information that a programmer can use to determine why an exception was thrown. At the same time, the catch block can decide what measures to take based on this information. For example, here is a design for an exception raised by the function hmean( ):

// exc_mean.h  -- exception classes for hmean(), gmean()
#include <iostream>
 
class bad_hmean {
private:
    double v1;
    double v2;
public:
    bad_hmean(double a = 0, double b = 0) : v1(a), v2(b){}
    void mesg();
};
 
inline void bad_hmean::mesg() {   
    std::cout << "hmean(" << v1 << ", " << v2 <<"): "
              << "invalid arguments: a = -b\n";
}

//用法:
if(a == -b)
{
    throw bad_hmean(a,b);
}

//函数gmean( )计算两个数的几何平均值,即乘积的平方根。这个函数要求两个参数都不为负,如果参数为负,它将引发异常
class bad_gmean {
public:
    double v1;
    double v2;
    bad_gmean(double a = 0, double b = 0) : v1(a), v2(b){}
    const char * mesg();
};
 
inline const char * bad_gmean::mesg() {  
    return "gmean() arguments should be >= 0\n";
}

try{
    ...
}
catch(bad_hmean & bg)
{
    ...
}
catch(bad_gmean & hg)
{
    ...
}

If the function hmean( ) throws a bad_hmean exception, the first catch block will catch the exception; if gmean( ) throws a bad_gmean exception, the exception will escape the first catch block and be caught by the second catch block.

//error4.cpp – using exception classes
#include <iostream>
#include <cmath> // or math.h, unix users may need -lm flag
#include "exc_mean.h"
// function prototypes
double hmean(double a, double b);
double gmean(double a, double b);
int main() {
    using std::cout;
    using std::cin;
    using std::endl;
    double x, y, z;
    cout << "Enter two numbers: ";
    while (cin >> x >> y) {
        try {                  // start of try block
            z = hmean(x,y);
            cout << "Harmonic mean of " << x << " and " << y
                << " is " << z << endl;
            z = gmean(x,y);
            cout << "Geometric mean of " << x << " and " << y
                << " is " << z << endl;
            cout << "Enter next set of numbers <q to quit>: ";
        } catch (bad_hmean & bg) { // start of catch block
            bg.mesg();
            cout << "Try again.\n";
            continue;
        } catch (bad_gmean & hg) {
            cout << hg.mesg();
            cout << "Values used: " << hg.v1 << ", " 
                 << hg.v2 << endl;
            cout << "Sorry, you don't get to play any more.\n";
            break;
        } // end of catch block
    }
    cout << "Bye!\n";
    // cin.get();
    // cin.get();
    return 0;
}
 
double hmean(double a, double b) {
    if (a == -b)
        throw bad_hmean(a,b);
    return 2.0 * a * b / (a + b);
}
 
double gmean(double a, double b) {
    if (a < 0 || b < 0)
        throw bad_gmean(a,b);
    return std::sqrt(a * b); 
}

First, the bad_hmean exception handler uses a continue statement, while the bad_gmean exception handler uses a break statement. Therefore, if the parameters provided by the user to the function hmean( ) are incorrect, the program will skip the remaining codes in the loop and enter the next loop; and when the parameters provided by the user to the function gmean( ) are incorrect, the loop will end. This demonstrates how the program can determine which exception was thrown (based on the exception type) and act accordingly.

Second, the exception classes bad_gmean and bad_hmean use different techniques. Specifically, bad_gmean uses public data and a public method that returns a C-style string.

1.6 Exception Specifications and C++11

Exception specification, which is a new feature of C++98, but C++11 has abandoned it. This means that C++11 is still in the standard, but may be removed from the standard in the future, so you are not recommended to use it. However, before ignoring the exception specification, you should at least know what it looks like, as follows:

double harm(double a) throw(bad_thing);// may throw bad_thing exception
double marm(double) throw();// doesn't throw an exception

The throw() part is the exception specification, which may appear in the function prototype and function definition, and may or may not contain a list of types.

One of the functions of an exception specification is to tell the user that a try block may be required. However, this can also be done easily using annotations.

Another function of the exception specification is to let the compiler add code that performs runtime checks to check whether the exception specification is violated. This is difficult to check. For example, marm() might not throw an exception, but it might call a function that calls another function that throws an exception. Also, your function doesn't throw an exception when you code it, but it does when the library is updated. All in all, the consensus in the programming community (especially developers trying to write secure code) is that it's best not to use this feature. And C++11 also recommends that you ignore exception specifications.

However, C++11 does support a special exception specification: you can use the new keyword noexcept to indicate that a function does not throw an exception:

double marm() noexcept;// marm() doesn't throw an exception

There is some debate as to whether this exception specification is necessary and useful, with some arguing that it is better not to use it (at least in most cases); Throwing an exception helps the compiler optimize the code. By using this keyword, the programmer writing the function is making a promise.

There is also the operator noexcept(), which determines whether its operand throws an exception.

1.7 Stack unwinding

1.7.1 The difference between return and throw

Return means return. Each function calls another function, and the instruction address of the called function will be stored in the stack of the calling function. The function is called layer by layer, and the stack is also stacked layer by layer. When returning, the current function returns to the superior function , it will not be returned to the upper-level function until the upper-level function returns, and the function stack of return will be released at the same time, and so on.

Throw is to throw an exception. Its target is no longer the upper layer function, but the try block. Where is the try block? Where does it stop? When calling functions layer by layer, the underlying function throw will release the stack in order until it encounters try piece.

In a simple sentence: the function calling return returns to the upper level, and the upper level will not be returned until the return is encountered in the upper level, and the statement before the return of the upper level is executed as usual. Call throw to release the stack from bottom to top until a try block is encountered in a certain level of function, and other code parts of the intermediate level function cannot continue to execute.

1.7.2 What is stack unwinding

Assuming that the try block does not directly call the function that raised the exception, but instead calls a function that makes a call to the function that raised the exception, the program flow jumps from the function that raised the exception to the function that contains the try block and handler. This involves stack unwinding.

First, let's take a look at how C++ usually handles function calls and returns.

C++ usually handles function calls by putting information on the stack (see Chapter 9). Specifically, the program puts the address of the instruction that called the function (the return address) on the stack. When the called function finishes executing, the program uses this address to determine where to continue execution. In addition, function calls place function parameters on the stack. On the stack, these function parameters are treated as automatic variables. If the called function creates new automatic variables, those variables will also be added to the stack. If the called function calls another function, the latter's information is added to the stack, and so on. When the function ends, the program flow will jump to the address stored when the function was called, and the top element of the stack will be freed. Therefore, functions typically return to the function that called it, and so on, with each function releasing its automatic variables at the end. If the automatic variable is a class object, the class's destructor (if any) will be called.

Now assuming that the function terminates due to an exception (rather than due to a return), the program will also free the memory on the stack, but instead of stopping after freeing the first return address of the stack, it will continue to free the stack until it finds a location at The return address in the try block (see Figure 15.3). Control then goes to the exception handler at the end of the block, not to the first statement after the function call. This process is called stack unwinding.

A very important feature of the raising mechanism is that, as with function return, for automatic class objects on the stack, the class's destructor will be called. However, function returns only deal with the objects that the function put on the stack, while the throw statement deals with the objects put on the stack by the entire sequence of function calls between the try block and throw. If there is no such feature as stack unwinding, after an exception is thrown, the destructor of the automatic class object placed on the stack by the intermediate function call will not be called.

insert image description here

1.8 Other anomalous properties

Although the throw-catch mechanism is similar to the function parameter and function return mechanism, there are some differences.

One of these is that the return statement in the function fun( ) returns control to the function that called fun(), but the throw statement returns control up to the first such function: a try-catch combination that catches the corresponding exception .

Another difference is that the compiler always creates a temporary copy when an exception is thrown, even if references are specified in exception specifications and catch blocks.

class problem{};

void super()throw(problem)
{
    if(oh_no){
        problem oops;
        throw oops;
    }
}

try{
    super();
}
catch(problem &p)
{
    
}

p will point to a copy of oops rather than oops itself. This is a good thing, because after the function super() finishes executing, oops will no longer exist. By the way, it would be simpler to combine raising an exception and creating an object:

throw problem();

You might ask, why use references in the code since the throw statement will make a copy? After all, the usual reason for having a reference as a return value is to avoid creating copies for efficiency.

The answer is that references have another important feature: base class references can execute derived class objects. Assuming a set of exception types related by inheritance, only one base class reference needs to be listed in the exception specification, and it will match any derived class objects.

Assuming that there is an exception class hierarchy and different exception types are to be handled separately, using the base class reference will be able to capture any exception object; and using the derived class object can only capture the class to which it belongs and the class derived from this class object.

The raised exception object will be caught by the first catch block that matches it. This means that catch blocks should be listed in the reverse order of derivation:

class bad_1{}
class bad_2 : public bad_1{...}
class bad_3 : public bad_2{...}

void duper()
{
    if(oh_no){
        throw bad_1();
    }
    if(rats){
        throw bad_2();
    }
    if(drat){
        throw bad_3();
    }
}

try{
    duper();
}
catch(bad_3 &be)
{//statements
}
catch(bad_2 &be)
{//statements
}
catch(bad_1 &be)
{//statements
}

If you put bad_1 & handler first, it will catch exceptions bad_1, bad_2 and bad_3; by doing it in reverse order, bad_3 exception will be caught by bad_3 & handler.

If you have an exception class inheritance hierarchy, you should arrange the catch blocks so that the catch statement that catches the exception class at the bottom of the hierarchy is
placed first, and the catch statement that catches the base class exception is placed last.

By ordering the catch blocks correctly, it gives you choice in how exceptions are handled. However, sometimes it may not be known which exceptions to expect. For example, suppose you write a function that calls another function, and you don't know which exceptions the called function might throw. In this case, the exception can still be caught, even if the type of the exception is not known. The method is to use an ellipsis to denote the exception type, thus catching any exception:

catch(...){}

If you know some exceptions that may be thrown, you can put the above catch block that catches all exceptions at the end, which is somewhat similar to the default in the switch statement:

try {
    duper();
} catch (bad_3 &be) {
    // statements
} catch (bad_2 &be) {
    // statements
} catch (bad_1 &be) {
    // statements
} catch (...) {
    // statements
}

1.9 excepyion class

The main purpose of C++ exceptions is to provide language-level support for designing fault-tolerant programs, that is, exceptions make it easier to include error handling functions in program design, so as not to adopt some strict error handling methods after the fact. The flexibility and relative convenience of exceptions encourage programmers to add error handling functions in program design when conditions permit.

Newer C++ compilers incorporate exceptions into the language. To support the language, the exception header file (formerly exception.h or except.h) defines the exception class, which C++ can use as a base class for other exception classes. Your code can throw exceptions, or you can use the exception class as a base class. There is a virtual member function called what( ) that returns a string whose characteristics vary by implementation. However, since this is a virtual method, it can be redefined in classes derived from exception:

#include <exception>
class bad_hmean : public std::exception {
public:
    const char* what() { return "bad arguments to hmean()"; }
...
};
 
class bad_gmean : public std::exception {
public:
    const char* what() { return "bad arguments to gmean()"; }
...
};

If you don't want to handle these derived exceptions differently, you can catch them in the same base class handler:

try {
    ...
} catch (std::exception& e {
    cout << e.what() << endl;
    ...
}

Otherwise, they can be captured separately. The C++ library defines many exception-based exception types.

1.9.1 stdexcept exception class

The header file <stdexcept> defines several other exception classes. First, the file defines the logic_error and runtime_error classes, both of which are publicly derived from exception:

class logic_error : public exception {
public:
    explicit logic_error(const string& what_arg);
    explicit logic_error(const char* what_arg);
...
};
 
class domain_error : public logic_error {
public:
    explicit domain_error(const string& what_arg);
    explicit domain_error(const char* what_arg);
...
};

Note that the constructors of these classes accept a string object as a parameter, which provides the character data returned by the method what( ) as a C-style string.

These two new classes are used as base classes for two families of derived classes. The exception class family logic_error describes typical logic errors. In general, this kind of error can be avoided by reasonable programming, but in practice these errors can still occur. The name of each class indicates the type of error it is used to report:

  • domain_error;

Mathematical functions have a domain and a range. The domain consists of the possible values ​​of the parameters, and the range consists of the possible return values ​​of the function. For example, the sine function has a range from negative infinity to positive infinity because any real number has a sine value; but the sine function has a range from −1 to +1 because they are the largest and smallest sine values, respectively. The arcsine function, on the other hand, has a domain of definition from −1 to +1 and a range of values ​​from −π to +π. If you write a function that passes an argument to the function std::asin(), you can have the function raise a domain_error exception if the argument is not in the domain −1 to +1.

  • invalid_argument;

The exception invalid_argument indicates that an unexpected value was passed to the function. For example, if a function expects to accept a string in which each character is either a '0' or a '1', the function will raise an invalid_argument exception if the passed string contains other characters.

  • length_error;

The exception length_error is used to indicate that there is not enough space to perform the desired operation. For example, the append() method of the string class will throw a length_error exception when the length of the combined string exceeds the maximum allowable length.

  • out_of_bounds。

The exception out_of_bounds is typically used to indicate indexing errors. For example, you could define an array-like class whose operator()[] raises an out_of_bounds exception when an invalid index is used.

Each class has a unique logic_error-like constructor that allows you to provide a string for the method what( ) to return.

Next, the runtime_error exception family describes errors that may occur during runtime but are difficult to predict and prevent. The name of each class indicates the type of error it is used to report:

  • range_error;
  • overflow_error;
  • underflow_error。

Underflow (underflow) errors in floating-point calculations. In general, there is a minimum non-zero value that can be represented by a floating-point type, and calculation results smaller than this value will cause an underflow error. Both integer and floating-point types may have overflow errors. When the calculation result exceeds the maximum magnitude that a certain type can represent, an overflow error will occur. The result of the calculation may not be within the range allowed by the function, but no overflow or underflow error has occurred, in which case the range_error exception can be used.

Each class has a unique runtime_error-like constructor that allows you to provide a string for the method what( ) to return.

In general, the logic_error family of exceptions indicates a problem that can be fixed programmatically, while the runtime_error family of exceptions indicates an unavoidable problem. All these error classes have the same general characteristics, the main difference between them is that different class names allow you to handle each exception separately. Inheritance relationships, on the other hand, let you handle them together (if you want).

If the above library classes cannot meet your needs, you should derive an exception class from logic_error or runtime_error to ensure that your exception classes can be classified into the same inheritance hierarchy.

1.9.2 bad_alloc exception and new

For the memory allocation problem caused by using new, the latest C++ method is to let new raise a bad_alloc exception. The header file contains the declaration of the bad_alloc class, which is publicly derived from the exception class. But previously, new returned a null pointer when the requested amount of memory could not be allocated.

// newexcp.cpp -- the bad_alloc exception
#include <iostream>
#include <new>
#include <cstdlib>  // for exit(), EXIT_FAILURE
using namespace std;
 
struct Big {
    double stuff[20000];
};
 
int main() {
    Big * pb;
    try {
        cout << "Trying to get a big block of memory:\n";
        pb = new Big[10000]; // 1,600,000,000 bytes
        cout << "Got past the new request:\n";
    } catch (bad_alloc & ba) {
        cout << "Caught the exception!\n";
        cout << ba.what() << endl;
        exit(EXIT_FAILURE);
    }
    cout << "Memory successfully allocated\n";
    pb[0].stuff[0] = 4;
    cout << pb[0].stuff[0] << endl;
    delete [] pb;
    // cin.get();
    return 0; 
}

img

1.9.3 Null pointers and new

A lot of code is written when new returns a null pointer on failure. To deal with the change of new, some compilers provide a flag (switch) that allows the user to choose the desired behavior. Currently, the C++ standard provides a new that returns a null pointer on failure, which is used as follows:

int* pi = new (std::nothrow) int;
int* pa = new (std::nowthrow) int[500];
Big* pb = new (std::nothrow) Big[10000];
if (pb == 0) {
    cout << "Could not allocate memory. Bye.\n";
    exit(EXIT_FAILURE);
}

1.10 Exceptions, classes and inheritance

Exceptions, classes, and inheritance are related to each other in three ways. First, one exception class can be derived from another, as the standard C++ library does; second, exception class declarations can be nested within class definitions to combine exceptions; third, such nested declarations can themselves be inherited, Can also be used as a base class.

1.11 When Exceptions Get Lost

After the exception is thrown, it can cause problems in two situations.

  • First, if it is raised in a function with an exception specification, it must match some kind of exception in the specification list (in the inheritance hierarchy, the class type matches an object of this class and its derived classes), otherwise called Unexpected exception. By default, this will cause the program to terminate abnormally (although C++11 dropped the exception specification, it is still supported and some existing code uses it).

  • If the exception is not thrown in a function (or the function has no exception specification), it must be caught. If it is not caught (as would be the case when there is no try block or no matching catch block), the exception is called an uncaught exception. By default, this will cause the program to terminate abnormally.

    However, it is possible to modify the program's reaction to unexpected and uncaught exceptions. Let's see how to modify it, starting with uncaught exceptions.

1.11.1 Uncaught Exceptions

Uncaught exceptions do not cause immediate program termination. Instead, the program will first call the function terminate( ). By default, terminate( ) calls the abort( ) function. This behavior of terminate( ) can be modified by specifying a function that terminate( ) should call instead of abort( ). To do this, call the set_terminate( ) function. Both set_terminate( ) and terminate( ) are declared in the header file exception:

typedef void (*terminate_handler)();
terminate_handler set_terminate(terminate_handler f) throw();// C++98
terminate_handler set_terminate(terminate_handler f) noexcept;// C++11
void terminate();// C++98
void terminate() noexcept;// C++11

The typedef in it makes terminate_handler the name of such a type: a pointer to a function with no parameters and no return value. The set_terminate() function takes as a parameter the name (address) of a function that takes no parameters and returns type void, and returns the address of the function. If the set_terminate() function is called multiple times, terminate() will call the function set by the last set_terminate() call.

Let's look at an example. Suppose you want an uncaught exception to cause the program to print a message and then call the exit() function, setting the exit status value to 5.

#include <exception>
using namespace std;
...
void myQuit() {
    cout << "Terminating due to uncaught exception\n";
    exit(5);
}
...
int main() {
    set_terminate(myQuit);
...
};

Now, if an exception is thrown and not caught, the program will call terminate(), which will call MyQuit().

1.11.2 Unexpected exceptions

Next look at unexpected exceptions. By specifying an [exception specification] to a function, users of the function know which exceptions to catch. Suppose the prototype of the function is as follows:

double Argh(double, double) throw(out_of_bounds);

Then you can use the function like this:

try {
    x = Argh(a, b);
} catch (out_of_bounds& ex) {
    ...
}

It is helpful to know which exceptions should be caught, because by default, uncaught exceptions will cause the program to terminate abnormally.

Unexpected exceptions behave very much like uncaught exceptions. If an unexpected exception occurs, the program will call the unexpected() function. This function will call terminate(), which by default will call abort(). Just as there is a set_terminate() function that can be used to modify the behavior of terminate(), there is also a set_unexpected() function that can be used to modify the behavior of unexpected(). These new functions are also declared in the header file:

typedef void (*unexpected_handler)();
unexpected_handler set_unexpected(unexpected_handler f) throw();// C++98
unexpected_handler set_unexpected(unexpected_handler f) noexcept;// C++11
void unexpected();// C++98
void unexpected() noexcept;// C++11

However, the behavior of the functions supplied to set_unexpected() is more strictly restricted than the behavior of the functions supplied to set_terminate(). Specifically, the unexpected_handler function can:

  • Terminate the program by calling terminate() (the default behavior), abort() or exit();
  • throws an exception.

The result of raising an exception (the second option) depends on the exception thrown by the unexpected_handler function and the [exception specification] of the function that threw the unexpected exception:
if the newly raised exception matches the original [exception specification], the program will start from there Do normal processing, i.e. look for a catch block that matches the newly raised exception. Basically, this method will replace unexpected exceptions with expected exceptions;

If the newly raised exception does not match the original [Exception Specification], and the [Exception Specification] does not include the std::bad_exception type, the program will call terminate(). bad_exception is derived from exception, and its declaration is located in the header file <exception>;

If the newly raised exception does not match the original [Exception Specification], and the original [Exception Specification] contains the std::bad_exception type, the unmatched exception will be replaced by the std::bad_exception exception.

In summary, if you want to catch all exceptions (whether expected or unexpected), you can do this:

#include <exception>//首先确保异常头文件的声明可用:
using namespace std;
...
void myUnexcepted() {//然后,设计一个替代函数,将意外异常转换为bad_exception异常,该函数的原型如下
    throw std::bad_exception();// or just throw
}//仅使用throw,而不指定异常将导致重新引发原来的异常。然而,如果异常规范中包含了这种类型,则该异常将被bad_exception对象所取代。

 
double Argh(double, double) throw(out_of_bounds, bad_exception);
 
int main() {
    set_unexpected(myUnexcepted);//在程序的开始位置,将意外异常操作指定为调用该函数:
    try {
        x = Argh(a, b);//最后,将bad_exception类型包括在异常规范中,并添加如下catch块序列:
    } catch (out_of_bounds& ex) {
        ...
    } catch (bad_exception& ex) {
        ...
    }
}

1.12 Notes on exceptions

  • Exception specification does not apply to templates, because the exceptions thrown by a template function may vary with a particular reification.
  • Exceptions and dynamic memory allocation don't always work together.

Dynamic memory allocation and exceptions are discussed further below. First, look at the following function:

void test1(int n) {
    string mesg("I'm trapped in an endless loop");
    ...
    if (oh_no)
        throw exception();
    ...
    return;
}

The string class uses dynamic memory allocation. Normally, string's destructor will be called for mesg when the function ends. Although the throw statement prematurely terminates the function, it still causes the destructor to be called, thanks to the unwinding of the stack. So here, memory is properly managed.

Next look at the following function:

void test2(int n) {
    double* ar = new double[n];
    ...
    if (oh_no)
        throw exception();
    ...
    delete[] ar;
    return;
}

There is a problem here. When unwinding the stack, the variable ar in the stack will be deleted. But the premature termination of the function means that the delete[] statement at the end of the function is ignored. The pointer is gone, but the block of memory it points to is not freed and is inaccessible. In short, the memory is leaked.

This leakage is avoidable. For example, you can catch the exception in the function that throws it, include some cleanup code in the catch block, and re-throw the exception:

void test3(int n) {
    double* ar = new double[n];
    ...
    try {
        if (oh_no)
            throw exception();
    } catch (exception& ex) {
        delete[] ar;
        throw;
    }
    ...
    delete[] ar;
    return;
}

However, this will increase the chance of oversights and other errors. Another workaround is to use one of the smart pointer templates discussed in Chapter 16.

In short, although exception handling is extremely important for some projects, it can also increase the workload of programming, increase the size of the program, and slow down the speed of the program. On the other hand, the cost of not doing error checking can be very high.

exception handling

In modern libraries, the complexity of exception handling can reach new heights - mainly because the documentation does not explain the exception handling routines or explains them poorly. Anyone who has worked proficiently with a modern operating system has encountered errors and problems caused by unhandled exceptions. The programmers behind these errors usually face an uphill battle of constantly understanding the intricacies of the library: what exceptions will be raised, why and when they occur, how to handle them, and so on.

Novice programmers will quickly find that understanding exception handling in a library is as difficult as learning the language itself, and that the routines and patterns contained in modern libraries can be as foreign and difficult as the details of C++ syntax. To develop good software, you must spend time understanding the intricacies of libraries and classes, just as you must spend time learning C++ itself. The programmer and his software benefit from the details of exception and error handling learned through the library documentation and source code.

Guess you like

Origin blog.csdn.net/weixin_43717839/article/details/131362718