为什么 C++ 标准要费心发明std::exception
类?他们有什么好处?我问的原因是这样的:
try
{
throw std::string("boom");
}
catch (std::string str)
{
std::cout << str << std::endl;
}
工作正常。稍后,如果需要,我可以制作自己的轻量级“异常”类型。那么我为什么要打扰std::exception
呢?
为什么 C++ 标准要费心发明
std::exception
类?他们有什么好处?
它提供了一个通用且一致的接口来处理标准库抛出的异常。标准库生成的所有异常都继承自std::exception
.
请注意,标准库 api 可能会抛出许多不同类型的异常,举几个例子:
std::bad_alloc
std::bad_cast
std::bad_exception
std::bad_typeid
std::logic_error
std::runtime_error
std::bad_weak_ptr | C++11
std::bad_function_call | C++11
std::ios_base::failure | C++11
std::bad_variant_access | C++17
等等......
std::exception
是所有这些异常的基类:
为所有这些异常提供基类,允许您使用通用异常处理程序处理多个异常。
如果需要,我可以制作自己的轻量级“异常”类型。那么我为什么要打扰std::exception
呢?
如果您需要自定义异常类,请继续制作一个。但是std::exception
让你的工作更轻松,因为它已经提供了很多好的异常类应该具备的功能。它使您可以轻松地从它派生并为您的类功能覆盖必要的功能(特别是)。std::exception::what()
这为您提供了std::exception
处理程序的 2 个优势,
为什么 C++ 标准要费心发明 std::exception 类?他们有什么好处?
拥有不同类型的异常可以让您捕获特定类型的错误。从公共基础派生异常允许根据情况捕获更通用或特定错误的粒度。
在 C++ 中,现有的类型系统已经存在,因此当您可以在语言中显式创建所需类型的异常时,不需要标准化错误字符串。
std::exception及其派生类的存在有两个主要原因:
标准库必须具有某种异常层次结构才能在异常情况下抛出。总是抛出一个std::string是不合适的,因为你没有干净的方法来定位特定类型的错误。
为库供应商提供一个可扩展的基于类的接口,以抛出最基本的错误类型并为用户提供通用的回退。您可能希望提供比简单的what()字符串更多的错误元数据,以便发现错误的人可以更智能地从中恢复。
同时,如果用户只关心该错误消息,则作为通用基础的std::exception允许一般包罗万象,而不是...。
如果您所做的只是打印并退出,那么这并不重要,但是您也可以使用继承自std::exception的std::runtime_error以方便捕获。
稍后,如果需要,我可以制作自己的轻量级“异常”类型。那么我为什么要打扰 std::exception 呢?
如果您从std::runtime_error继承并使用您自己的自定义错误类型,那么您可以追溯添加错误元数据,而无需重写 catch 块!相反,如果您曾经更改过错误处理设计,那么您将被迫重写所有std::string捕获,因为您无法安全地从std::string继承。这不是一个前瞻性的设计决定。
如果现在看起来还不错,想象一下,如果您的代码作为共享库在多个项目之间共享,并且有各种程序员在处理它。迁移到新版本的库会很痛苦。
这甚至没有提到 std::string 可以在复制、构造或访问字符期间抛出自己的异常!
Boost 的网站在这里有一些关于异常处理和类构建的良好指南。
我正在编写一些网络代码并使用第三方供应商的库。在用户输入的无效 IP 地址上,此库会引发自定义异常nw::invalid_ip从 std::runtime_error派生。nw::invalid_ip包含一个描述错误消息的what(),但也包含提供的wrong_ip()地址。
我还使用std::vector来存储套接字,并利用检查的 at()调用来安全地访问索引。我知道如果我在一个超出范围的值上调用at() ,则会抛出std::out_of_range。
我知道也可能会抛出其他东西,但我不知道如何处理它们,或者它们到底是什么。
当我收到nw::invalid_ip错误时,我会弹出一个模式,其中包含一个输入框,供用户使用无效的 IP 地址填充,以便他们可以编辑它并重试。
对于std::out_of_range问题,我通过对套接字运行完整性检查并修复不同步的向量/套接字关系来做出响应。
对于任何其他std::exception问题,我会使用错误日志终止程序。最后我有一个catch(...)记录“未知错误!” 并终止。
仅抛出std::string很难稳健地执行此操作。
这是在不同情况下抛出一些东西的基本示例,因此您可以尝试捕获异常。
#include <vector>
#include <iostream>
#include <functional>
#include <stdexcept>
#include <bitset>
#include <string>
struct Base1 {
virtual ~Base1(){}
};
struct Base2 {
virtual ~Base2(){}
};
class Class1 : public Base1 {};
class Class2 : public Base2 {};
class CustomException : public std::runtime_error {
public:
explicit CustomException(const std::string& what_arg, int errorCode):
std::runtime_error(what_arg),
errorCode(errorCode){
}
int whatErrorCode() const {
return errorCode;
}
private:
int errorCode;
};
void tryWrap(typename std::function<void()> f){
try {
f();
} catch(CustomException &e) {
std::cout << "Custom Exception: " << e.what() << " Error Code: " << e.whatErrorCode() << std::endl;
} catch(std::out_of_range &e) {
std::cout << "Range exception: " << e.what() << std::endl;
} catch(std::bad_cast &e) {
std::cout << "Cast exception: " << e.what() << std::endl;
} catch(std::exception &e) {
std::cout << "General exception: " << e.what() << std::endl;
} catch(...) {
std::cout << "What just happened?" << std::endl;
}
}
int main(){
Class1 a;
Class2 b;
std::vector<Class2> values;
tryWrap([](){
throw CustomException("My exception with an additional error code!", 42);
});
tryWrap([&](){
values.at(10);
});
tryWrap([&](){
Class2 c = dynamic_cast<Class2&>(a);
});
tryWrap([&](){
values.push_back(dynamic_cast<Class2&>(a));
values.at(1);
});
tryWrap([](){
std::bitset<5> mybitset (std::string("01234"));
});
tryWrap([](){
throw 5;
});
}
Custom Exception: My exception with an additional error code! Error Code: 42
Range exception: vector::_M_range_check
Cast exception: std::bad_cast
Cast exception: std::bad_cast
General exception: bitset::_M_copy_from_ptr
What just happened?
这是一个合理的问题,因为std::exception
实际上只包含一个属性:what()
, a string
。所以很容易只使用string
而不是exception
. 但事实是 anexception
不是 a string
。如果您将异常视为仅仅是一个string
,您将失去从提供更多属性的专用异常类中派生的能力。
例如,今天您string
在自己的代码中抛出 s。明天您决定为某些情况添加更多属性,例如数据库连接异常。您不能仅仅从衍生而来string
做出这种改变;您将需要编写一个新的异常类并更改string
. Usingexception
是异常处理程序仅使用他们关心的数据的一种方式,可以在需要处理它们时挑选和选择异常。
此外,如果您只抛出和处理string
-typed 异常,您将错过任何不是您自己的代码抛出的所有异常。如果这种区别是有意的,最好使用通用异常类来表示这一点,而不是string
.
exception
也比string
. 这意味着库开发人员可以编写接受异常作为参数的函数,这比接受string
.
所有这些基本上都是免费的,只需使用exception
而不是string
.
仅仅因为 6 行玩具示例中的“工作正常”并不意味着它在实际代码中是可扩展或可维护的。
考虑这个函数:
template<typename T>
std::string convert(const T& t)
{
return boost:lexical_cast<std::string>(t);
}
bad_alloc
如果无法分配字符串的内存, 这可能会抛出,或者bad_cast
如果转换失败,则可能会抛出。
此函数的调用者可能想要处理表示输入错误但不是致命错误的失败转换情况,但不想处理内存不足的情况,因为他们对此无能为力,所以让异常向上传播堆栈。这在 C++ 中很容易做到,例如:
std::string s;
try {
s = convert(val);
} catch (const std::bad_cast& e) {
s = "failed";
}
如果只是抛出异常,因为std::string
代码必须是:
std::string s;
try {
s = convert(val);
} catch (const std::string& e) {
if (e.find("bad_cast") != std::string::npos)
s = "failed";
else
throw;
}
这需要更多代码来实现,并依赖于异常字符串的确切措辞,这可能取决于编译器实现和boost::lexical_cast
. 如果系统中的每一个异常处理都必须进行字符串比较来确定此时是否可以处理错误,那将是混乱且无法维护的。在引发异常的系统的一个部分中对异常消息的拼写进行微小更改可能会导致系统另一部分中的异常处理代码停止工作。这会在错误位置和每个系统中的一些错误处理代码。使用异常的优点之一是允许将错误处理与主逻辑分开,如果您基于整个系统中的字符串比较创建依赖关系,您将失去该优势。
C++ 中的异常处理通过匹配异常的类型来捕获事物,它不会通过匹配异常的值来捕获事物,因此抛出不同类型的事物以允许细粒度处理是有意义的。抛出单个字符串类型的东西并根据字符串的值处理它们是混乱的、不可移植的并且更加困难。
稍后,如果需要,我可以制作自己的轻量级“异常”类型。那么我为什么要打扰 std::exception 呢?
如果您的代码有用且可重用并且我想在我的系统的一部分中使用它,我是否必须添加捕获所有轻量级类型的异常处理?为什么我的整个系统都应该关心系统的某个部分所依赖的库的内部细节?如果您的自定义异常类型是从中派生的,那么我可以在不知道(或关心)特定类型std::exception
的情况下捕获它们。const std::exception&
哇,我很惊讶没有人提到这一点:
您需要多种类型的异常才能区分它们——某些类型的异常应该被处理,而另一些则不应该。
它们需要有一个通用的基类,以便您能够向用户显示合理的消息,而不必知道您的程序可能抛出的所有类型的异常(这在使用外部封闭源代码库时是不可能的)。
如果您是该类的唯一用户,则可以避免std::exception
(如果您想避免标准库异常)
但是,如果您的课程将被其他人(程序员)使用,他们将如何处理异常。
如果您的类throws
astring
描述了错误,那将无济于事,因为您的类的消费者更喜欢更标准的方式(捕获异常对象并查询它是什么方法)来处理exception
而不是捕获字符串。
您还可以通过捕获exception
对象来捕获标准库引发的异常
您可以覆盖异常what
类的方法以提供有关错误的更多信息。