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();
}
最后会输出相关错误内容并终止程序。
需要注意的是:
- 回调函数必须终止程序,可以使用定义在
<cstdlib>
中的abort()
或_Exit(xxx)
完成,它们会在 不清理资源 的情况下终止程序。 - 设置新的
terminate_handler()
时,set_terminate()
会返回旧的terminate_handler()
。如果有些场景需要用到旧的回调函数,建议保存一下。 - 在专门编写的软件中,通常会设置这个崩溃回调,在进程结束前创建崩溃转储,包含调用栈、异常、日志等信息。
编写自己的异常类
自定义异常类
编写自己的异常类有两个好处:
- 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_exception
和MyException
派生而来,是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高级编程