Detailed explanation of "C++" exception

The "Preface" article is about C++ exceptions, let's start to explain

"Belonging Column" C Quack

"Author" Mr. Maple Leaf (fy)

"Motto" Cultivate myself on the way forward

"Mr. Maple Leaf is a little intellectually ill"

" One sentence per article " 

The shoulders of a young man should be like this,
don't worry about anything,
first stir up Qingfengmingyue, Yangliu Yiyi and Caochang Yingfei,
the shoulders of a young man
should be full of beautiful things.
——Princes of Fenghuo Opera "Sword Come"

Table of contents

1. The traditional way of handling errors in C language

2. C++ exception concept

3. Abnormal use

3.1 Exception throwing and catching

3.2 Exception re-throwing

3.3 Exception Safety

3.4 Exception specification

4. Custom exception system

Five, the exception system of the C++ standard library

6. Abnormal advantages and disadvantages


1. The traditional way of handling errors in C language

The traditional error handling mechanism of C language:

  1. Termination program : such as assert, defect: difficult for users to accept. If a memory error occurs, the division by zero error will terminate the program.
  2. Return error code : There is a defect, and the programmer needs to find the corresponding error by himself. For example, the interface functions of many libraries in the system indicate errors by putting error codes in errno, and the error prompts are not obvious

tips: assert will be invalid in release version 

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

2. C++ exception concept

Exception is a way of handling errors. When a function finds an error that it cannot handle, it can throw an exception to let the direct or indirect caller of the function handle the error. Using exceptions to handle errors does not terminate the program

The keywords about exceptions are introduced as follows: 

  • throw: When a problem occurs, the program will throw an exception. This is done by using the throw keyword
  • try: The code in the try block identifies the specific exception that will be activated, usually followed by one or more catch blocks
  • catch: Where you want to handle the problem, catch the exception through the exception handler. The catch keyword is used to catch exceptions, and there can be multiple catches to catch

If a block throws an exception, the method to catch the exception will use the try and catch keywords. The code that may throw an exception is placed in the try block, and the code in the try block is called protected code.

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

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

3. Abnormal use

3.1 Exception throwing and catching

Exception throwing and matching principles:

  1. An exception is raised by throwing an object whose type determines which catch's handling code should be activated.
  2. The selected handler is the one in the call chain that matches the object type 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. (The processing here is similar to the value return of the function)
  4. catch(...) can catch any type of exception , the problem is that it doesn't know what the exception error is.
  5. There is an exception to the matching principle of throwing and capturing in practice. Not all types match exactly. Derived class objects that can be thrown can be captured using the base class. This is very practical in practice, and we will explain this in detail later

Test below 

(1) For the first point: An exception is thrown by throwing an object whose type determines which catch's processing code should be activated

Test the code, when an exception is thrown: the object thrown below is a string, which is stored in the constant area, and the corresponding catch type is const char*, which cannot be char*. If it is char*, the authority will be enlarged

double Division(int a, int b)
{
	// 当b == 0时抛出异常
	if (b == 0)
		throw "Division by zero condition!";
	else
		return ((double)a / (double)b);
}
void Func()
{
	int len, time;
	cin >> len >> time;
	cout << Division(len, time) << endl;
}
int main()
{
	try 
	{
		// try 块中放置可能抛出异常的代码
		Func();
	}
	catch (const char* errmsg)// catch 关键字用于捕获异常
	{
		cout << errmsg << endl;
	}

	return 0;
}

Run, enter 10 2, the exception code is not executed, and the program runs normally

Run, input 10 0, a division by 0 error occurs, an exception is thrown, catch catches the exception, the catch option type matches, executes the code in the catch, prints an error message, and the program ends normally; otherwise, it does not execute and terminates the program directly

Modify const char* errmsg to char* errmsg, run, input 10 0, a division by 0 error occurs, an exception is thrown, the catch option type does not match, capture fails, and the program terminates

(2) catch(...) can catch any type of exception

Add catch(...) after the catch, if the catch option type does not match, it will go directly to the catch(...) statement block, which also solves the problem of the catch option type mismatch, and the program will be normal Running, no termination will occur, because the statement block of catch(...) is reached

(3) If there are multiple try/catch nests, the catch option types all match, and the selected processing code is the one in the call chain that matches the object type and is closest to the position where the exception is thrown

test code

int main()
{
	try 
	{
		// try 块中放置可能抛出异常的代码
		try
		{
			Func();
		}
		catch (const char* errmsg)//类型匹配,就近原则
		{
			cout << "1234" << errmsg << endl;
		}	
	}
	catch (const char* errmsg)//类型匹配
	{
		cout << errmsg << endl;
	}
	catch (...) 
	{
		cout << "Unkown Exception" << endl;
	}

	return 0;
}

Run the input 10 0, a division by 0 error occurs, and the catch statement block closest to the position where the exception is thrown is used

Exception stack expansion matching principle in function call chain

  1. First check whether the throw itself is inside the try block, and if so, look for a matching catch statement. If there is a match, it will be transferred to the catch for processing.
  2. If there is no matching catch, exit the current function stack, and continue to search for matching catches in the stack of the calling function.
  3. If the stack of the main function is reached and there is still no match, the program is terminated. The above process of finding a matching catch clause along the call chain is called stack unwinding. So in practice, we have to add a catch(...) at the end to catch any type of exception, otherwise when there is an exception that is not caught, the program will terminate directly.
  4. After the matching catch clause is found and processed, it will continue to execute along the catch clause

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 << "Unkown Exception:未知异常" << endl;
	}
	return 0;
}

When the exception in func1 is thrown:

  • First, it will check whether the throw itself is inside the try block. 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 searches in the function stack where main is located, and finally finds 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

 

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

3.2 Exception re-throwing

It is possible that a single catch cannot completely handle an exception. After some corrective processing, it is hoped that it will be handed over to the outer call chain function for processing. The catch can pass the exception to the upper layer function for processing by rethrowing

test code

void func1()
{
	try
	{
		throw string("除0异常");
	}
	catch(const string& s)
	{
		//打印提示信息,简单处理
		cout << s << endl;
		// 重新抛出,让外层处理错误
		throw;
	}
}
int main()
{
	try
	{
		func1();
	}
	catch (const string& s)
	{
		cout << "进行处理..." << s << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception:未知异常" << endl;
	}
	return 0;
}

As a result of the operation, the exception is thrown to the outer layer, and the outer layer handles it

Directly letting the outer layer catch exceptions for processing may cause some problems. for example:

void func1()
{
	//...
	throw string("除0异常");
}

void func2()
{
	int* array = new int[10];
	func1();

	//...

	delete[] array;
    cout << "内存已释放" << endl;
}
int main()
{
	try
	{
		func1();
	}
	catch (const string& s)
	{
		cout << s << ", 进行处理..." << endl;
	}
	catch (...)
	{
		cout << "Unkown Exception:未知异常" << endl;
	}
	return 0;
}

In func2, a piece of memory space is applied for through the new operator, and the space is released through delete at the end of func2, but because an exception is 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, which causes the memory block requested in func2 to not be released, resulting in a memory leak

The result of the operation, the memory is not released

 

At this time, the exception thrown by func1 can be captured in func2 first, and after the capture, the requested memory is released first and then the exception is rethrown, which avoids memory leaks

modify the code

void func2()
{
	int* array = new int[10];
	try
	{
		func1();
	}
	catch (...)
	{
		cout << "delete []" << array << endl;
		delete[] array;
		throw;
	}
	//...
	delete[] array;
	cout << "array内存已释放" << endl;
}

As a result of the operation, the exception is rethrown after the memory is released.

3.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 cleans up 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 exceptions thrown in new and delete, resulting in memory leaks, exceptions thrown between lock and unlock lead to deadlocks, C++ often uses RAII to solve the above problems, about RAII, smart pointers are explained in this section

3.4 Exception specification

The purpose of the exception specification is to let the user of the function know what exceptions the function may throw.

This is how C++98 does it: 

  1. Throw (type) can be followed by the function to list all exception types that may be thrown by this function
  2. The function is followed by throw(), indicating that the function does not throw an exception
  3. If no exception interface is declared, this function can throw any type of exception
  4. However, these norms are written in a complicated way, and these norms are not mandatory, and in the end they are useless

for example: 

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

The C++11 approach

  • The new noexcept in C++11 means that no exception will be thrown
  • This is a simplification of the complex writing of C++98's exception specification

For example:

// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

4. Custom exception system

In actual use, many companies will customize their own exception system for standardized exception management and exception module division, because if everyone throws exceptions at will in a project, then the outer caller will basically have no way to play, so in practice they will Define a set of inherited specification systems. In this way, what everyone throws is an inherited derived class object, and it is enough to capture a base class

The most basic exception class needs to contain at least two member variables of error number and error description

For example, there are the following exception base classes:

// 服务器开发中通常使用的异常继承体系
class Exception
{
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{}
	virtual string what() const
	{
		return _errmsg;
	}
protected:
	string _errmsg;
	int _id;
};

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 SqlException : public Exception
{
public:
	SqlException(const string& errmsg, int id, const string& sql)
		:Exception(errmsg, id)
		, _sql(sql)
	{}
	virtual string what() const
	{
		string str = "SqlException:";
		str += _errmsg;
		str += "->";
		str += _sql;
		return str;
	}
private:
	const string _sql;
};
class CacheException : public Exception
{
public:
	CacheException(const string& errmsg, int id)
		:Exception(errmsg, id)
	{}
	virtual string what() const
	{
		string str = "CacheException:";
		str += _errmsg;
		return str;
	}
};
class HttpServerException : public Exception
{
public:
	HttpServerException(const string& errmsg, int id, const string& type)
		:Exception(errmsg, id)
		, _type(type)
	{}
	virtual string what() const
	{
		string str = "HttpServerException:";
		str += _type;
		str += ":";
		str += _errmsg;
		return str;
	}
private:
	const string _type;
};
void SQLMgr()
{
	srand(time(0));
	if (rand() % 7 == 0)
	{
		throw SqlException("权限不足", 100, "select * from name = '张三'");
	}
	//throw "xxxxxx";
}
void CacheMgr()
{
	srand(time(0));
	if (rand() % 5 == 0)
	{
		throw CacheException("权限不足", 100);
	}
	else if (rand() % 6 == 0)
	{
		throw CacheException("数据不存在", 101);
	}
	SQLMgr();
}
void HttpServer()
{
	// ...
	srand(time(0));
	if (rand() % 3 == 0)
	{
		throw HttpServerException("请求资源不存在", 100, "get");
	}
	else if (rand() % 4 == 0)
	{
		throw HttpServerException("权限不足", 101, "post");
	}
	CacheMgr();
}

Test below

#include <windows.h>
int main()
{
	while (1)
	{
		Sleep(1000);
		try {
			HttpServer();
		}
		catch (const Exception& e) // 这里捕获父类对象就可以
		{
			// 多态
			cout << e.what() << endl;
		}
		catch (...)
		{
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

test run

 

Five, the exception system of the C++ standard library

C++ provides a series of standard exceptions, defined in std::exception, we can use these standard exceptions in the program. They are organized in a parent-child class hierarchy as follows

 

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

Explanation: In practice, we can inherit the exception class to implement our own exception class. But in reality, many companies define a set of exception inheritance system like the above, because the C++ standard library design is not easy to use

 I won't introduce much else

6. Abnormal advantages and disadvantages

Advantages of C++ exceptions:

  1. The exception object is defined. Compared with the error code, it can clearly and accurately display various error information, and even include stack call information, 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 errors layer by layer, and the outermost layer can get the error. See the detailed explanation below.
  3. Many third-party libraries contain exceptions, such as boost, gtest, gmock and other commonly used libraries, so we need to use exceptions when using them.
  4. It is easier to handle some functions using exceptions. For example, the constructor does not return a value, which is inconvenient to use error codes. For example, for a function like T& operator, if pos is out of bounds, it can only use an exception or terminate the program processing, and there is no way to indicate an error through the return value.

Disadvantages of C++ exceptions:

  1. Exceptions will cause the execution flow of the program to jump wildly , and it is very chaotic, and it will jump randomly when an error is thrown at runtime. This will make it difficult for us to track and debug and analyze the program.
  2. Exceptions have some performance overhead. Of course, in the case of fast modern hardware, this effect 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 handle resource management issues. Learning costs are higher.
  4. The exception system of the C++ standard library is not well defined, 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, and the users captured by the outer layer will be miserable. So there are two points in the exception specification: First, the types of exceptions thrown are all inherited from a base class. 2. Whether or not the function throws an exception and what kind of exception it throws is normalized in the way of fun() noexcept.

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

--------------------- END ----------------------

「 作者 」 枫叶先生
「 更新 」 2023.4.23
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。

Guess you like

Origin blog.csdn.net/m0_64280701/article/details/130319319