3 - 其他操作

本文将简要介绍C++异常的一些其他操作,包括如何编写处理未捕获异常的回调函数,如何编写自己的异常类,如何嵌套异常等。

未捕获的异常的回调函数

如果抛出的异常没有经过捕获处理,程序会调用内建的terminate()函数,这个函数调用<cstdlib>中的abort()来终止程序。可调用set_terminate()来设置自己的回调函数terminate_handler(),它们均在<exception>中声明,它们的运行原理如下:

try
{
    main(argc, argv);
}
catch (...)
{
    // 如果自定义回调函数存在, 则进行自定义回调
    if (terminate_handler != nullptr)
    {
        terminate_handler();
    }
    else
    {
        terminate();
    }
}

接下来尝试编写自定义的terminate_handler()

[[noreturn]] void myTerminate()
{
	cerr << "Uncaught exception!!!" << endl;
	_Exit(1);
}

然后在main()中应用它即可:

int main()
{
	set_terminate(myTerminate);

	throw exception();
}

最后会输出相关错误内容并终止程序。

需要注意的是:

  1. 回调函数必须终止程序,可以使用定义在<cstdlib>中的abort()_Exit(xxx)完成,它们会在 不清理资源 的情况下终止程序。
  2. 设置新的terminate_handler()时,set_terminate()会返回旧的terminate_handler()。如果有些场景需要用到旧的回调函数,建议保存一下。
  3. 在专门编写的软件中,通常会设置这个崩溃回调,在进程结束前创建崩溃转储,包含调用栈、异常、日志等信息。

编写自己的异常类

自定义异常类

编写自己的异常类有两个好处:

  • C++标准库提供的异常数目有限,不好表达程序员的具体需求。
  • 可在异常中加入自己的信息,而标准库提供的大多异常只允许设置错误信息字符串,而不是其他数据结构。

建议自己编写的异常类直接/间接从exception类继承,这样更方便于用多态处理异常。

以文件相关的异常为例,这里先创建一个通用的文件错误类FileError

class FileError : public exception
{
public:
	FileError(string filename) : m_filename(move(filename)) {}
	const char* what() const noexcept override { return m_message.c_str(); }
	virtual const string& getFilename() const { return m_filename; }
protected:
	virtual void setMessage(string message) { m_message = move(message); }
private:
	string m_filename;
	string m_message;
};

接下来创建一个具体的文件错误类,无法打开文件时的FileOpenError

class FileOpenError : public FileError
{
public:
	FileOpenError(string filename) : FileError(move(filename))
	{
		setMessage(format("Unable to open {}.", getFilename()));
	}
};

以及读取文件时发生错误的FileReadError

class FileReadError : public FileError
{
public:
	FileReadError(string filename, size_t lineNumber)
		: FileError(move(filename)), m_lineNumber(lineNumber)
	{
		setMessage(format("Error reading {}, line {}.", getFilename(), m_lineNumber));
	}

	virtual size_t getLineNumber() const noexcept { return m_lineNumber; }
private:
	size_t m_lineNumber = 0;
};

接下来,就能利用多态来捕获刚刚写的自定义异常了:

try
{
    // ...
}
catch (const FileError& e)
{
    cerr << e.what() << endl;
    return 1;
}

获取源码位置

在C++20之前,可用以下预处理宏获取源代码中的位置信息:

描述
__FILE__当前源代码的文件名
__LINE__当前源代码所处行号

此外,每个函数都有一个局部定义的静态字符数组__func__,它包含函数的名字。

C++20在<source_location>中,以std::source_location类的形式,为上边的信息引入了一个适当的面向对象的替代品。该类有如下公有方法:

方法描述
.file_name()当前源代码的文件名
.function_name()如果当前位置在函数中,则包含当前函数名
.line()当前源代码所处的行号
.column()当前源代码所处的列号

利用静态方法.current()可以直接在方法被调用的源代码位置上创建source_location实例:

int main()	// <<<第7行
{
	const source_location& location = source_location::current();	// <<<第9行
	cout << format("{}({}): {}: {}", location.file_name(),
				   location.line(), location.function_name(), "Test message") << endl;
}
// 输出
main.cpp(9): int __cdecl main(void): Test message

好好利用source_location,可以在自定义异常类中存储抛出异常的位置:

class MyException : public exception
{
public:
    MyException(string message, 
                source_location location = source_location::current())
        : m_message(move(message)), m_location(move(location))
    {}
    
    const char* what() const noexcept override { return m_message.c_str(); }
    virtual const source_location& where() const noexcept { return m_location; }
private:
    string m_message;
    source_location m_location;
}

嵌套异常

当处理一个异常时,有可能会触发新异常,从而要求抛出第二个异常,此时正在处理的第一个异常的所有信息都会丢失。C++用嵌套异常(nested exception)来解决这一问题,它允许将捕获的异常嵌套到新的环境。例如,假设调用第三方库的某函数抛出了异常种类A,实际上想用异常种类B,可以把A嵌入到B中。嵌套异常在<exception>中定义。

可以使用std::throw_with_nested()进行异常的嵌套,下例将runtime_error嵌入到自定义异常MyException(上边写的,但没有source_location)中:

try
{
    throw runtime_error("I am a runtime_error");
}
catch (const runtime_error& e)
{
    cout << "Caught a runtime_error!" << endl;
    cout << "Throwing myException" << endl;
    throw_with_nexted(MyException("I am a myException"));
}

throw_with_nested()抛出一个编译器生成的未命名异常类,这个类型由nested_exceptionMyException派生而来,是C++中有用的多重继承的另一个示例。nested_exception基类通过调用std::current_exception()自动捕获正在处理的异常,并将其存储在std::exception_ptr中。exception_ptr是一种类似指针的类型,可以存储空指针或用current_exception()捕获的异常对象的指针。

下例将试图获取被嵌套的runtime_error,通过使用dynamic_cast()

try
{
    // 上边抛出嵌套异常的例子
}
catch (const MyException& e)
{
    cout << "Caught MyException" << endl;
    
    // 如果内部有嵌套类,就不是空指针
    const auto* nested = dynamic_cast<const nested_exception*>(&e);
    if (nested)
    {
        try
        {
            // 重新抛出嵌套异常, 并捕获
            nested->rethrow_nested();
        }
        catch (const runtime_error& e)
        {
            cout << "Caught runtime_error in MyException!" << endl;
        }
	}
}

实际上,还能用std::rethrow_if_nested()这一辅助函数简化代码:

try
{
    // 上边抛出嵌套异常的例子
}
catch (const MyException& e)
{
    cout << "Caught MyException" << endl;
    try
    {
        rethow_if_nested(e);
    }
    catch (const runtime_error& e)
    {
        cout << "Caught runtime_error in MyException!" << endl;
    }
}

参考资料

  • 飘零的落花 - 现代C++详解
  • C++20高级编程