[C++11] Exception

1. The traditional way of handling errors in C language

Traditional error handling mechanism:

  1. Terminate the program, such as assert. Defects: It is difficult for users to accept. If a memory error occurs, the division by zero error will terminate the program.
  2. Return an error code. Defect: It is necessary for the programmer to find the corresponding error by himself. For example, the interface functions of many libraries of the system indicate errors by putting error codes in errno.
  3. The combination of setjmp and longjmp in the C standard library. (uncommonly used)

In practice, the C language basically uses the method of returning error codes to handle errors, and in some cases uses the termination program to handle very serious errors.

2. C++ exception concept

Exception is a common way of handling errors in object-oriented languages. When a function finds an error that it cannot handle, it can throw an exception, allowing the direct or indirect caller of the function to handle the error.

  • throw: When a problem occurs in the program, an exception can be thrown through the throw keyword.
  • try: The code that may throw an exception is placed in the try block. The code block will perform exception error detection during execution. The try block is usually followed by one or more catch blocks.
  • catch: If an error occurs in the try block, you can define the corresponding code block to be executed in the catch block.

The syntax for using a try/catch statement is as follows −

try
{
    
    
	//被保护的代码
}
catch (ExceptionName e1)
{
    
    
	//catch块
}
catch (ExceptionName e2)
{
    
    
	//catch块
}
catch (ExceptionName eN)
{
    
    
	//catch块
}

3. Exception usage

1. Exception throwing and catching

The matching principle of exception throwing and catching:

  1. An exception is thrown by throwing an object. The type of the object determines which catch processing code should be activated. If the thrown exception object is not caught, or if there is no matching type of catch, the program will terminate and report an error.
  2. The selected handler (catch block) is the one in the call chain that matches the type of the object and is closest to where the exception was thrown.
  3. After the exception object is thrown, a copy of the exception object will be generated, because the thrown exception object may be a temporary object, so a copy object will be generated, and the copied temporary object will be destroyed after being caught. (Similar to function return by value)
  4. catch(...)Any type of exception can be caught, but there is no way to know what the exception error is after it is caught.
  5. There is an exception to the matching principle of throwing and catching of actual exceptions. The exception types caught and thrown do not have to match exactly. You can throw derived class objects and use the base class to catch them. This is very useful in practice.

The matching principle of exception stack expansion in the function call chain:

  1. When an exception is thrown, first check whether the throw itself is inside the try block, if so, find a matching catch statement, and if there is a match, jump to the catch for processing.
  2. If the current function stack does not have a matching catch, exit the current function stack and continue to search for a matching catch in the previous calling function stack. After the matching catch clause is found and processed, it will continue to execute along the catch clause without jumping back to the original place where the exception was thrown.
  3. If the stack of the main function is reached and no matching catch is found, the program is terminated.

For example, in the following code, func3 is called in the main function, func2 is called in func3, func1 is called in func2, and an exception object of type string is thrown in func1.

void func1()
{
    
    
	throw string("这是一个异常");
}
void func2()
{
    
    
	func1();
}
void func3()
{
    
    
	func2();
}
int main()
{
    
    
	try
	{
    
    
		func3();
	}
	catch (const string& s)
	{
    
    
		cout << "错误描述:" << s << endl;
	}
	catch (...)
	{
    
    
		cout << "未知异常" << endl;
	}
	return 0;
}

When the exception in func1 is thrown:

  • First, it will check whether throw itself is inside the try block. Here, since throw is not inside the try block, it will exit the function stack where func1 is located, and continue to search in the previous calling function stack, that is, the function stack where func2 is located.
  • Since there is no matching catch in func2, it will continue to search in the previous calling function stack, that is, the function stack where func3 is located.
  • There is no matching catch in func3, so it will search in the function stack where main is located, and finally find a matching catch in the main function stack.
  • At this time, it will jump to the corresponding catch block in the main function to execute the corresponding code block, and continue to execute the subsequent code of the code block after execution.

As shown below:

insert image description here

The above process of finding a matching catch clause along the call chain is called stack unwinding. In practice, we have to add an catch(...)exception to catch any type at the end, otherwise when there is an exception that is not caught, the program will terminate directly.

2. Exception rethrowing

Sometimes a single catch may not be able to completely handle an exception. After some correction processing, it is hoped that it will be handed over to the outer call chain function for processing. For example, the outermost layer may need to get the exception to record the log information. At this time It is necessary to pass the exception to the upper layer function for processing by rethrowing.

But if you directly let the outermost layer catch the exception for processing, it may cause some problems. for example:

void func1()
{
    
    
	throw string("这是一个异常");
}
void func2()
{
    
    
	int* array = new int[10];
	func1();

	//do something...

	delete[] array;
}
int main()
{
    
    
	try
	{
    
    
		func2();
	}
	catch (const string& s)
	{
    
    
		cout << s << endl;
	}
	catch (...)
	{
    
    
		cout << "未知异常" << endl;
	}
	return 0;
}

Among them, func2 applied for a memory space through the new operator, and released the space through delete at the end of func2, but because an exception was thrown inside func1 called by func2, it will directly jump to the main function at this time The catch block executes the corresponding exception handler, and continues to execute along the catch block after processing.

At this time, the memory block requested in func2 is not released, resulting in a memory leak. At this time, the exception thrown by func1 can be captured first in func2, and after the capture, the requested memory is released first and then the exception is rethrown, which avoids memory leaks. for example:

void func2()
{
    
    
	int* array = new int[10];
	try
	{
    
    
		func1();
		//do something...
	}
	catch (...)
	{
    
    
		delete[] array;
		throw; //将捕获到的异常再次重新抛出
	}
	delete[] array;
}

Explain:

  • Other types of exceptions may be thrown between new and delete in func2, so it is best to capture them in the best way in fun2 catch(...), delete the requested memory and then rethrow it through throw
  • When re-throwing the exception object, the exception object to be thrown may not be specified after throw (it just happens that I don't know catch(...)what exception object is captured in the way).

3. Exception safety

The security problems caused by throwing exceptions are called abnormal security problems. Here are some suggestions for abnormal security problems:

  1. The constructor completes the construction and initialization of the object. It is best not to throw an exception in the constructor, otherwise the object may be incomplete or not fully initialized.
  2. The destructor mainly completes the cleanup of object resources. It is best not to throw an exception in the destructor, otherwise it may cause resource leaks (memory leaks, unclosed handles, etc.).
  3. Exceptions in C++ often lead to resource leaks, such as throwing exceptions in new and delete, causing memory leaks, and throwing exceptions between lock and unlock to cause deadlocks. C++ often uses RAII to solve the above problems.

Attachment: For more learning about exception safety, please refer to this article C++ exception safety

4. Exception specification

In order to let function users know what types of exceptions a function may throw, the C++ standard stipulates:

  1. After the function throw(type1, type2, ...), list all exception types that this function may throw.
  2. After the function throw()or noexcept(C++11), it means that the function does not throw an exception.
  3. If no exception interface is declared, this function can throw any type of exception. (exception interface declaration is not mandatory)

for example:

//表示func函数可能会抛出A/B/C/D类型的异常
void func() throw(A, B, C, D);
//表示这个函数只会抛出bad_alloc的异常
void* operator new(std::size_t size) throw(std::bad_alloc);
//表示这个函数不会抛出异常
void* operator new(std::size_t size, void* ptr) throw();

4. Custom exception system

In practice, many companies customize their own exception system for standardized exception management.

  • Projects in the company are generally divided into modules, allowing different programmers or teams to complete different modules. If exception throwing is not regulated, then the programmer responsible for catching exceptions at the outermost layer will be very uncomfortable, because he needs Catch all types of exception objects thrown by everyone.
  • Therefore, in practice, a set of inheritance specification system will be defined. First, define a basic exception class. The exception objects thrown by everyone must be derived class objects inherited from the exception class, because the exception syntax stipulates that it can be captured by the base class. The derived class object thrown, so the outermost layer only needs to capture the base class.

As shown below:

insert image description here

The most basic exception class needs to contain at least two member variables of error number and error description, and can even contain information such as the call chain of the current function stack frame. Generally, the exception class also provides two member functions, which are used to obtain the error number and error description respectively. for example:

class Exception
{
    
    
public:
	Exception(int errid, const char* errmsg)
		:_errid(errid)
		, _errmsg(errmsg)
	{
    
    }
	int GetErrid() const
	{
    
    
		return _errid;
	}
	virtual string what() const
	{
    
    
		return _errmsg;
	}
protected:
	int _errid;     //错误编号
	string _errmsg; //错误描述
	//...
};

If other modules want to extend this exception class, they must inherit this basic exception class. You can add some member variables to the inherited exception class as needed, or rewrite the inherited virtual function what to make it Can inform the programmer more exception information. for example:

class CacheException : public Exception
{
    
    
public:
	CacheException(int errid, const char* errmsg)
		:Exception(errid, errmsg)
	{
    
    }
	virtual string what() const
	{
    
    
		string msg = "CacheException: ";
		msg += _errmsg;
		return msg;
	}
protected:
	//...
};
class SqlException : public Exception
{
    
    
public:
	SqlException(int errid, const char* errmsg, const char* sql)
		:Exception(errid, errmsg)
		, _sql(sql)
	{
    
    }
	virtual string what() const
	{
    
    
		string msg = "CacheException: ";
		msg += _errmsg;
		msg += "sql语句: ";
		msg += _sql;
		return msg;
	}
protected:
	string _sql; //导致异常的SQL语句
	//...
};

Explain:

  • Member variables of exception classes cannot be made private because private members are not visible in subclasses.
  • The what member function in the base class Exception is best defined as a virtual function, which is convenient for subclasses to rewrite it, so as to achieve the effect of polymorphism.

5. Standard library exception system

The exceptions in the C++ standard library are also a basic system, where exception is the base class of each exception class. We can use these standard exceptions in the program. The inheritance relationship between them is as follows:

insert image description here

The following table is a description of each exception that occurs in the above inheritance system:

abnormal describe
std::exception This exception is the parent class of all standard C++ exceptions.
std::bad_alloc This exception can be thrown by new.
std::bad_cast This exception can be thrown by dynamic_cast.
std::bad_exception This is very useful when dealing with unexpected exceptions in C++ programs.
std::bad_typeid This exception can be thrown by typeid.
std::logic_error Anomalies that can theoretically be detected by reading the code.
std::domain_error This exception is thrown when an invalid math field is used.
std::invalid_argument This exception is thrown when an invalid parameter is used.
std::length_error This exception is thrown when a std::string that is too long is created.
std::out_of_range This exception can be thrown by methods such as std::vector and std::bitset<>::operator.
std::runtime_error Abnormalities that cannot theoretically be detected by reading the code.
std::overflow_error This exception is thrown when a mathematical overflow occurs.
std::range_error This exception is thrown when an attempt is made to store a value out of range.
std::underflow_error This exception is thrown when a mathematical underflow occurs.

Explain:

  • The what member function and destructor of the exception class are defined as virtual functions, which is convenient for subclasses to rewrite them, so as to achieve the effect of polymorphism.
  • In practice, we can also inherit the exception class to implement our own exception class, but in practice, many companies will define their own exception inheritance system.

6. Abnormal advantages and disadvantages

Advantages of exceptions:

  1. The exception object is defined. Compared with the error code, it can clearly and accurately display various error information, and even include information such as stack calls, which can help better locate program bugs.
  2. A big problem with the traditional way of returning error codes is that in the function call chain, if the deep function returns an error, then we have to return error codes layer by layer, and finally the outermost layer can get the error.
  3. Many third-party libraries use exceptions, such as boost, gtest, gmock and other commonly used libraries. If we don't use exceptions, we can't play the role of these libraries well.
  4. Many testing frameworks also use exceptions, so using exceptions can better use unit tests for white-box testing.
  5. Some functions are better handled by using exceptions. For example T& operator, if pos exceeds the limit, you can only use exceptions or terminate the program processing, and there is no way to indicate errors through return values.

Exceptional disadvantages:

  1. Exceptions will cause the execution flow of the program to jump around and be very confusing, which will make it difficult for us to track, debug and analyze the program.
  2. Exceptions will have some performance overhead, of course, in the case of fast modern hardware, this impact is basically negligible.
  3. C++ does not have a garbage collection mechanism, and resources need to be managed by themselves. With exceptions, it is very easy to cause abnormal security problems such as memory leaks and deadlocks. This requires the use of RAII to deal with resource management issues, and the learning cost is relatively high.
  4. The exception system of the C++ standard library is not defined well enough, causing everyone to define their own exception system, which is very confusing.
  5. Use exceptions as standard as possible, otherwise the consequences will be unimaginable, throwing exceptions at will will also make the users captured by the outer layer miserable.
  6. The exception interface declaration is not mandatory. For a function that does not declare an exception type, it is impossible to predict whether the function will throw an exception.

But in general, the advantages of exceptions outweigh the disadvantages, so we still encourage the use of exceptions in engineering, and OO languages ​​basically use exceptions to handle errors, which can also be seen that this is the general trend.


This is the end of this article, the code text is not easy, please support me a lot! ! !

Guess you like

Origin blog.csdn.net/weixin_67401157/article/details/132223744