[The Road to C++ Practice] 31. Abnormal

insert image description here
Every day without dancing is a disappointment to life

1. The traditional way of handling errors in C language

Traditional error handling mechanism:

  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 , defect: programmers need to find the corresponding error by themselves. For example, the interface functions of many libraries of the system indicate errors by putting error codes in errno.
    In practice, 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 way of handling errors. 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, the program will throw an exception. This is done using the throw keyword.

  • catch: Where you want to handle the problem, catch the exception through the exception handler. The catch keyword is used to catch the exception, and there can be multiple catches to catch.

  • try: The code in the try block identifies the specific exception that will be activated, and it is usually followed by one or more catch blocks.


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 块
}

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 you don’t know what the exception error is, so the exception caught here is an unknown exception, which is generally the last line of defense for exception capture.
  5. There is an exception to the matching principle of throwing and capturing in practice. Not all types match exactly. Objects of derived classes that can be thrown can be captured using the base class. This is very practical in practice. (used to handle exceptions thrown by multiple groups)

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 is transferred to the catch for processing.
  2. If there is no matching catch, exit the current function stack, and continue to search for a matching catch 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(...) to catch any type of exception at the end, 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.

image-20230329170518737

#include<iostream>
using namespace std;

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 {
    
    
		Func();
	}
	catch (const char* errmsg) {
    
    //没有错误则跳过这个catch
		cout << errmsg << endl;
	}

	return 0;
}

No error catch part will not be executed

image-20230329165345442

The catch part will only be executed when throwimage-20230329170619555


If a throw is made and the type does not match when catching, then an error will occur: because no matching catch can be found, the program will be terminated if it cannot be found (this example involves the amplification of permissions)image-20230329171103079

If both match, then choose the closer one.

3.2 Exception re-throwing

If try and catch are also performed in Func, then the priority will be entered to match the parameters first, and the code after the catch will continue to executeimage-20230329200226619


The role of exception capture

After the exception is caught, it can be executed normally, which can prevent memory leaks caused by code running errors. For example: if you comment out the try here, it will match the next one, and this part of the delete code will not be executed and cause memory leaks. leakage.

image-20230329202151704

Of course, there are other methods besides exceptions. Through smart pointers, smart pointers are used to handle this. (Follow-up)


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.

If you want to put every exception together to form a log, that is, to capture and process it uniformly, then use the method of rethrowing the exception.

For large-scale projects, the role of debugging is minimal, so the code needs to be maintained by logging.

#include<iostream>
using namespace std;

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* p1 = new int[10];
    try
    {
    
    
        int len, time;
        cin >> len >> time;
        cout << Division(len, time) << endl;
    }
    catch (const char* errmsg)
    {
    
    
        cout << "delete " << p1 << endl;
        delete[] p1;
        throw errmsg; // 重新抛出
    }
}
int main()
{
    
    
    try
    {
    
    
        Func();
    }
    catch (const char* errmsg)
    {
    
    
        cout << errmsg << endl; //重新抛出之后就可以在这里进行统一处理。
    }
    return 0;
}

image-20230329210500454


If there are too many types thrown, many catch statements will be needed, so in order to prevent such trouble, you can pass **…** so that any type can be matched. But the disadvantage is that you don't know what type of object it is, so you need to throw it again. Direct throw means throwing whatever is caught. This is actually the way to capture some actions that need to be processed after processing.

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

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* p1 = new int[10];
	try
	{
    
    
		int len, time;
		cin >> len >> time;
		cout << Division(len, time) << endl;
	}
	catch (...)
	{
    
    
		//cout << errmsg << endl;
		cout << "delete " << p1 << endl;
		delete[] p1;

		throw;// 重新抛出,捕获什么就抛出什么
	}

}
int main()
{
    
    
	try
	{
    
    
		Func();
	}
	catch (const char* errmsg)
	{
    
    //没有错误则跳过这个catch
		cout << errmsg << endl;
	}
	catch (int errid)
	{
    
    
		cout << errid << endl;
	}

	return 0;
}

image-20230330124332981

3.3 Exception Safety

  • 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
  • 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.)
  • Exceptions in C++ often lead to resource leaks, such as throwing exceptions in new and delete, resulting in memory leaks, and throwing exceptions between lock and unlock to cause deadlocks. C++ often uses RAII to solve the above problems.

3.4 Exception specification

For exceptions, if there is a try-catch statement for each function in the process of calling the function if it does not follow certain specifications, the approximate format is as follows:

image-20230619184500824

If no exception is thrown when A calls B, B calls C, and C calls D, but an exception is thrown in D (throw is not written, the above is just the format) but it is not caught due to type mismatch or other reasons, then at this time It is not only D that has an exception, A, B, and C will also throw exceptions one after another. Therefore, in order to control which function throws an exception, which function does not throw an exception, and what type of exception is thrown, the following method is produced:

Solution:

  1. The purpose of the exception specification is to let the user of the function know what exceptions the function may throw.
    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. (The control function does not throw an exception)
  3. If no exception interface is declared, this function can throw any type of exception.
// 这里表示这个函数会抛出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();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

Note: The [] internal check of vector is an assertion, and the at of vector is to throw an exception.

4. Custom exception system

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

image-20230330150844650

// 服务器开发中通常使用的异常继承体系
class Exception
{
    
    
public:
	Exception(const string& errmsg, int id)
		:_errmsg(errmsg)
		, _id(id)
	{
    
    }
	virtual string what() const
	{
    
    
		return _errmsg;
	}
protected:
	string _errmsg;
	int _id;
};
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();
}
int main()
{
    
    
	while (1)
	{
    
    
		Sleep(1000);//单位:ms

		try {
    
    
			HttpServer();
		}


		catch (const Exception& e) // 这里捕获父类对象就可以
		{
    
    
			// 多态
			cout << e.what() << endl;
		}
		catch (const CacheException& e)//上面匹配了,这里就不会匹配了
		{
    
    
			cout << e.what() << endl;
		}
		catch (...)
		{
    
    
			cout << "Unkown Exception" << endl;
		}
	}
	return 0;
}

5. The exception system of the C++ standard library

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

image-20230620133400965

image-20230620133417925

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

Code example:

#include<iostream>
#include<vector>

using namespace std;
int main()
{
    
    
	try {
    
    
		vector<int> v(10, 5);
		// 这里如果系统内存不够也会抛异常
		//v.reserve(1000000000);

		// 这里越界会抛异常
		v.at(10) = 100;
	}
	catch (const exception& e) // 这里捕获父类对象就可以
	{
    
    
		cout << e.what() << endl;
	}
	catch (...)
	{
    
    
		cout << "Unkown Exception" << endl;
	}

	return 0;
}

image-20230620133648822

image-20230620133711094

6. Abnormal advantages and disadvantages

advantage:

  1. Compared with error codes, exceptions can clearly and accurately display various error information, and even include stack call information, so as to better locate program bugs.
  2. There are problems in the traditional way of returning error codes, which have also been reflected in Linux system programming and network programming. In the function call chain, the deep function returns an error, so the outermost layer can get the error code by layer by layer. mistake:
// 1.下面这段伪代码我们可以看到ConnnectSql中出错了,先返回给ServerStart,ServerStart再返回给main函数,main函数再针对问题处理具体的错误。
    // 2.如果是异常体系,不管是ConnnectSql还是ServerStart及调用函数出错,都不用检查,因为抛出的异常异常会直接跳到main函数中catch捕获的地方,main函数直接处理错误。
int ConnnectSql()
{
    
    
    // 用户名密码错误
    if (...)
        return 1;
    // 权限不足
    if (...)
        return 2;
}
int ServerStart() {
    
    
    if (int ret = ConnnectSql() < 0)
        return ret;
    int fd = socket()
        if(fd < 0return errno;
}
int main()
{
    
    
    if(ServerStart()<0)
        ...
        return 0;
}
  1. 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.
  2. 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, a function such as T& operator, if the 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.

shortcoming:

  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 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 memory leaks, deadlocks and other abnormal security issues. This requires the use of RAII to deal with resource management issues.
  4. The exception system of the C++ standard library is not well defined, causing each 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 the function throws an exception and what kind of exception is thrown, use the method of func() throw(); to standardize.

Summary: Generally speaking, the advantages of exceptions outweigh the disadvantages, so we still encourage the use of exceptions in engineering. In addition, the language of the OS basically uses exceptions to handle errors.

Guess you like

Origin blog.csdn.net/NEFUT/article/details/131573290