Details of the exception handling mechanism in C++

  • 2020-05-17 06:03:50
  • OfStack

Exception handling

Improving error recovery is one of the most powerful ways to improve the robustness of your code. 1. The error handling methods used in the C language are considered tightly coupled, and the user of a function must write the error-handling code very close to the function call, making it unwieldily and unwieldily usable. Exception handling is introduced in C++, which is one of the main features of C++ and is a better way to think about problems and handle errors. There are some advantages to using error handling, as follows:

Instead of writing tedious error-handling code and mixing it with normal code, the programmer simply writes the code he wants to produce and then writes the error-handling code in a separate section later. Calling the same function multiple times requires only one error-handling code to be written somewhere.

Errors cannot be ignored if a function must send an error message to the caller once. It will throw an object that describes the error.

Traditional error handling and exception handling

Before we talk about exception handling, let's talk about traditional error handling in the C language. Here are three examples:

If an error is returned in a function, the function sets a global error status flag.

Signal processing system is made by using signals. In the function, raise signal is used and signal is used to set the signal processing function. In this way, the coupling degree is very high, and the signal values generated by different libraries may conflict

Using the non-local jump functions setjmp and longjmp from the standard C library, setjmp and longjmp are used to demonstrate error handling:


#include 
#include 
jmp_buf static_buf; // Used to hold the processor context for jumping 
void do_jmp()
 {
 //do something,simetime occurs a little error
 // call longjmp After that, it will load static_buf The processor information, and then the 2 The return point of the setjmp The return value of this function 
 longjmp(static_buf,10);//10 Is the error code, according to this error code to carry out the corresponding processing 
 }
int main()
 {
 int ret = 0;
 // Save the processor information to static_buf In the , And return 0 That's the same thing as doing it here 1 And then you can jump back 
 if((ret = setjmp(static_buf)) == 0) {
 // The code to execute 
 do_jmp();
 } else { // There is an error 
 if (ret == 10)
 std::cout << "a little error" << std::endl;
 }
 }

Error handling does not seem to be coupled very well, the normal code and error handling code are separated, and the processing code is all converged in 1. However, there is a serious problem in C++, which is that the object cannot be destructed and the destructor of the instantiated object will not be actively called after the local jump. This can lead to memory leaks. This is illustrated in the following example


#include 
#include 
using namespace std;
class base {
 public:
 base() {
 cout << "base construct func call" << endl;
 }
 ~base() {
 cout << "~base destruct func call" << endl;
 }
 };
jmp_buf static_buf;
void test_base() {
 base b;
 //do something
 longjmp(static_buf,47);// You jump, you'll find out b We can't destruct 
 }
int main() {
 if(setjmp(static_buf) == 0) {
 cout << "deal with some thing" << endl;
 test_base();
 } else {
 cout << "catch a error" << endl;
 }
 }

In the above code, only the constructor of base class will be called. When longjmp jumps, the instance of b will not be destructed, but the execution flow will not be able to return here. The instance of b will not be destructed. These are some of the problems that arise when local jumps are used to handle errors in C++, and exceptions don't occur in C++. So let's look at how to define an exception, throw an exception, and catch an exception.

Throwing of an exception


class MyError {
 const char* const data;
public:
 MyError(const char* const msg = 0):data(msg)
 {
 //idle
 }
};
void do_error() {
 throw MyError("something bad happend");
 }
int main()
 {
 do_error();
 }

In the above example, an instance of an exception class is thrown via throw. This exception class, which can be any custom class, can indicate the error message that occurred by instantiating the parameters passed in. An exception is just a class with an exception message. After the exception is thrown, it needs to be caught to recover from the error, so let's look at how to catch an exception. The biggest difference between using an exception for error handling in the above example and the previous implementation that used a local jump is that in the block of code thrown by the exception, the object is destructed, called a stack antisolution.

Exception catching

In C++, the catch keyword is used to catch the exception. After catching the exception, the exception can be processed. The statement block that is processed is called the exception handler. Here's a simple example of catching an exception:


try{
    //do something
    throw string("this is exception");
  } catch(const string& e) {
    cout << "catch a exception " << e << endl;
  }

catch like function, can have one parameter, throw thrown exception object, will be passed as a parameter to match to catch, then enter the exception handler, the above code shows just throw an exception, join try block could sell a variety of unusual, so how to deal with, here is that you can pick up more catch statement block, this will lead to introduce another one problem, that is how to match.

Abnormal matching

I think the matching of exception conforms to the principle of function parameter matching, but it is different. There is a type conversion when the function is matched, but there is no type conversion when the exception is matched. The following example illustrates this fact:


#include 
using namespace std;
 int main()
 {
 try{
 throw 'a';
 }catch(int a) {
 cout << "int" << endl;
 }catch(char c) {
 cout << "char" << endl;
 }
 }

The output of the code above is char, and since the type of exception thrown is char, it matches the second exception handler. You can see that no type conversion occurs during the matching process. Convert char to int. Although the exception handler does not do type conversion, the base class can be matched to a derived class. This is valid in both function and exception matching, but it is important to note that the parameter of catch needs to be either a reference type or a pointer type, otherwise it will cause the problem of cutting the derived class.


// The base class 
class Base{
 public:
 Base(string msg):m_msg(msg)
 {
 }
 virtual void what(){
 cout << m_msg << endl;
 }
 void test()
 {
 cout << "I am a CBase" << endl;
 }
 protected:
 string m_msg;
};
// A derived class that reimplements virtual functions 
class CBase : public Base
{
 public:
 CBase(string msg):Base(msg)
 {
 }
 void what()
 {
 cout << "CBase:" << m_msg << endl;
 }
 };
int main()
 {
 try {
 //do some thing
 // Throws a derived class object 
 throw CBase("I am a CBase exception");
 }catch(Base& e) { // It can be received using the base class 
 e.what();
 }
 }

The above code works just fine. In fact, when we write our own exception handling functions, we also implement custom byte exceptions by inheriting standard exceptions, but if we use Base & Instead of Base, it will cause the object to be cut. For example, the following code will compile incorrectly, because CBase is cut, so the test function in CBase cannot be called.


try {
    //do some thing
    throw CBase("I am a CBase exception");
}catch(Base e) {

e.test();

}

For this reason, the exception matching is made clear. To sum up, the exception matching basically follows the following rules:

In addition to having to be strictly type-matched, exception matching supports the following type conversions.

Allows nonconstant to constant type conversion, which means that you can throw a nonconstant type and then capture the corresponding constant type version using catch

Allows type conversion from derived classes to base classes

Allows arrays to be converted to array Pointers, and functions to be converted to function Pointers

Scenario 1 case, when I want to achieve 1 generation code, hope no matter what type of exception thrown me can be captured, for now we can only write 1 lot of catch statements capture any exceptions that may appear in the code to solve this problem, apparently to handle such too trival, fortunately C + + provides one can capture any exception mechanism, can use the following code in the grammar.

catch(...) {
// exception handler, where any exception can be caught, brings with it the problem of failure or exception information
}
If you're going to implement a library, you catch some exceptions in your library, but you just log them, and you don't handle them, and the exceptions are handled by the code that's called from above. Support is also provided for such a scenario as C++.


try{
    throw Exception("I am a exception");  
  }catch(...) {
    //log the exception
    throw;
  }

You can rethrow the currently caught exception by adding an throw to the catch block. In the exception throwing section 1, I threw an exception in the code, but I didn't use any catch statements to catch the exception that I threw. Executing the above program results in the following results.

terminate called after throwing an instance of 'MyError'
Aborted (core dumped)

Why did this happen ? , when we throw an exception, the exception will be as the relationship between function calls, level 1 level 1 throws up, won't stop until captured, if the end will lead to call terminate function not captured, the above output was the result of automatic call terminate function, in order to ensure greater flexibility, C + + provides set_terminate function can be used to set up their own terminate function. Once the setup is complete, the exception thrown if not caught is handled by the custom terminate function. Here is an example used:


#include 
#include 
#include 
using namespace std;
class MyError {
 const char* const data;
 public:
 MyError(const char* const msg = 0):data(msg)
 {
 //idle
 }
 };
void do_error() {
 throw MyError("something bad happend");
 }
 // The custom of terminate Function, function prototype required 1 to 
 void terminator()
 {
 cout << "I'll be back" << endl;
 exit(0);
 }
int main()
 {
 // Set the custom terminate , return is the original terminate A function pointer 
 void (*old_terminate)() = set_terminate(terminator);
 do_error();
 }

The code above will output I'll be back
Now that I've covered everything I know about exception matching, let's take a look at the next topic, resource cleanup in exceptions.

Resource cleanup in exceptions

On local jump, said local turned to won't call the object's destructor, can lead to memory leaks, C + + exceptions in does not have this problem, C + + through the stack the solution will be defined object destructor, but there is one exception is the constructor if abnormal, then this can lead to have allocated resources cannot be recycled, the following are examples of a constructor throws an exception:


#include 
#include 
using namespace std;
class base
 {
 public:
 base()
 {
 cout << "I start to construct" << endl;
 if (count == 3) // To construct the first 4 An exception is thrown when 
 throw string("I am a error");
 count++;
 }
 ~base()
 {
 cout << "I will destruct " << endl;
 }
 private:
 static int count;
 };
int base::count = 0;
int main()
 {
 try{
 base test[5];
 } catch(...){
 cout << "catch some error" << endl;
 }
 }

The output of the above code is:


#include 
#include 
using namespace std;
class base {
 public:
 base() {
 cout << "base construct func call" << endl;
 }
 ~base() {
 cout << "~base destruct func call" << endl;
 }
 };
jmp_buf static_buf;
void test_base() {
 base b;
 //do something
 longjmp(static_buf,47);// You jump, you'll find out b We can't destruct 
 }
int main() {
 if(setjmp(static_buf) == 0) {
 cout << "deal with some thing" << endl;
 test_base();
 } else {
 cout << "catch a error" << endl;
 }
 }
0

In the above code, the constructor has an exception, resulting in the corresponding destructor is not executed. Therefore, in the actual programming process, it should be avoided to throw an exception in the constructor. If there is no way to avoid it, then 1 must capture and process it in the constructor. The last one is the function try block. If the main function may throw an exception, how to catch it? How to catch an exception if the initialization list in the constructor might throw it. The following two examples illustrate the use of the function try statement block:


#include 
#include 
using namespace std;
class base {
 public:
 base() {
 cout << "base construct func call" << endl;
 }
 ~base() {
 cout << "~base destruct func call" << endl;
 }
 };
jmp_buf static_buf;
void test_base() {
 base b;
 //do something
 longjmp(static_buf,47);// You jump, you'll find out b We can't destruct 
 }
int main() {
 if(setjmp(static_buf) == 0) {
 cout << "deal with some thing" << endl;
 test_base();
 } else {
 cout << "catch a error" << endl;
 }
 }
1

The main function statement block, which can catch exceptions thrown in the main function.


#include 
#include 
using namespace std;
class base {
 public:
 base() {
 cout << "base construct func call" << endl;
 }
 ~base() {
 cout << "~base destruct func call" << endl;
 }
 };
jmp_buf static_buf;
void test_base() {
 base b;
 //do something
 longjmp(static_buf,47);// You jump, you'll find out b We can't destruct 
 }
int main() {
 if(setjmp(static_buf) == 0) {
 cout << "deal with some thing" << endl;
 test_base();
 } else {
 cout << "catch a error" << endl;
 }
 }
2

Much has been said above about the use of exceptions, how to define your own exceptions, whether exceptions should be written in accordance with the 1 standard, where to use exceptions, whether exceptions are safe, and more in the 1 series, which will be discussed in 11 below.

Standard exception

C + + standard library provides us with a series of standard exception, these standard exception from exception derived classes, mainly divides into two big derived class, one class is logic_error, another 1 kind is runtime_error these two classes in stdexcept header files, the former is mainly describe the program logic error in, passed an invalid parameter, for example, the latter refers to those errors caused by unforeseen events, such as hardware failures or run out of memory, etc., Both provide a constructor with a parameter type of std::string, so you can save the exception information and get it through the what member function.


#include 
#include 
#include 
using namespace std;
class MyError:public runtime_error {
 public:
 MyError(const string& msg = "") : runtime_error(msg) {}
};
//runtime_error logic_error  Both are inherited from standard exceptions with string The constructor 
 //
 int main()
 {
 try {
 throw MyError("my message"); 
 } catch(MyError& x) {
 cout << x.what() << endl; 
 }
 }

Abnormal specification

Assumes that a project using the 1 some third party libraries, so the 1 some functions of a third party libraries might throw an exception, but we don't know, so C + + provides a syntax, put a function may be the exception thrown out, so we when writing code to reference function of exception specification can, but C + + 11 of the exception specification of the scheme has been cancelled, so I don't want to too much introduction, through an example and see its basic usage, key see C + + 11 provide exception specification:


#include 
#include 
using namespace std;
class base {
 public:
 base() {
 cout << "base construct func call" << endl;
 }
 ~base() {
 cout << "~base destruct func call" << endl;
 }
 };
jmp_buf static_buf;
void test_base() {
 base b;
 //do something
 longjmp(static_buf,47);// You jump, you'll find out b We can't destruct 
 }
int main() {
 if(setjmp(static_buf) == 0) {
 cout << "deal with some thing" << endl;
 test_base();
 } else {
 cout << "catch a error" << endl;
 }
 }
4

The code above explains the basic syntax of the exception specification, what the unexpected function does, how to customize your unexpected function, and how to handle exceptions if they continue to be thrown from the unexpected function. This exception specification is removed from C++11. An noexcept function has been introduced to indicate whether the function will throw an exception


#include 
#include 
using namespace std;
class base {
 public:
 base() {
 cout << "base construct func call" << endl;
 }
 ~base() {
 cout << "~base destruct func call" << endl;
 }
 };
jmp_buf static_buf;
void test_base() {
 base b;
 //do something
 longjmp(static_buf,47);// You jump, you'll find out b We can't destruct 
 }
int main() {
 if(setjmp(static_buf) == 0) {
 cout << "deal with some thing" << endl;
 test_base();
 } else {
 cout << "catch a error" << endl;
 }
 }
5

In addition, noexecpt is provided to detect whether a function does not throw an exception.

Exception safety

I think the exception security is a rather complicated point, not only need to implement the function function, but also save the function will not be thrown in the case of an exception, not 1 to the state. Here to take one example, everyone in the implementation of the stack time often see the examples in the book defines a top function is used to get to the top of stack elements, and the return value is 1 void pop function only elements pop up the stack, so why not a pop function can pop up to the top of stack elements, and also can get to the top of stack elements & # 63;


#include 
#include 
using namespace std;
class base {
 public:
 base() {
 cout << "base construct func call" << endl;
 }
 ~base() {
 cout << "~base destruct func call" << endl;
 }
 };
jmp_buf static_buf;
void test_base() {
 base b;
 //do something
 longjmp(static_buf,47);// You jump, you'll find out b We can't destruct 
 }
int main() {
 if(setjmp(static_buf) == 0) {
 cout << "deal with some thing" << endl;
 test_base();
 } else {
 cout << "catch a error" << endl;
 }
 }
6

If the function throws an exception at the last line, this results in the function not returning the unloaded element, but Count has been reduced by 1, so the top element the function wants is lost. The essential reason is that this function tries to do two things at once, 1. Return a value, and 2. Change the state of the stack. It is best to put these two separate actions into two separate functions, following the principle of cohesive design, with each function doing only one thing. Let's talk about another exception-safe problem, which is how to write the assignment operator in a very common way, and how to ensure that the assignment operation is exception-safe.


#include 
#include 
using namespace std;
class base {
 public:
 base() {
 cout << "base construct func call" << endl;
 }
 ~base() {
 cout << "~base destruct func call" << endl;
 }
 };
jmp_buf static_buf;
void test_base() {
 base b;
 //do something
 longjmp(static_buf,47);// You jump, you'll find out b We can't destruct 
 }
int main() {
 if(setjmp(static_buf) == 0) {
 cout << "deal with some thing" << endl;
 test_base();
 } else {
 cout << "catch a error" << endl;
 }
 }
7

The above code does not have self-assignment security, if rhs is the object itself, then * rhs.pb will point to a deleted object. So ready to improve. Add identity test.


Widget& Widget::operator=(const Widget& rhs)
{
  If(this == rhs) return *this; // Identity test 
  delete pb;
  pb = new Bitmap(*rhs.pb);
  return *this;
}

However, the above code is still not exception-safe, because if an exception occurs during the execution of new Bitmap after the completion of delete pb, it will eventually point to a deleted block of memory. Now you can make the above code exception safe with a slight change of 1.


#include 
#include 
using namespace std;
class base {
 public:
 base() {
 cout << "base construct func call" << endl;
 }
 ~base() {
 cout << "~base destruct func call" << endl;
 }
 };
jmp_buf static_buf;
void test_base() {
 base b;
 //do something
 longjmp(static_buf,47);// You jump, you'll find out b We can't destruct 
 }
int main() {
 if(setjmp(static_buf) == 0) {
 cout << "deal with some thing" << endl;
 test_base();
 } else {
 cout << "catch a error" << endl;
 }
 }
9

This might seem like a simple example, but it's useful because there are a lot of situations where an assignment operator needs to be overloaded.


Related articles: