特殊异常类的必要性是什么?

7
为什么C++标准要发明std::exception类?它们有什么好处?我的提问原因是这样的:
try
{
  throw std::string("boom");
}
catch (std::string str)
{
  std::cout << str << std::endl;
}

代码可以正常工作。如果需要的话,我可以自己创建轻量级的“异常”类型。那为什么还要费心使用 std::exception


6
请注意,将异常捕获为引用而不是值是一种良好的编程实践。 - Andy Prowl
6个回答

11
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是所有这些异常的基类。

exceptions hierarchy

提供一个基类来处理所有这些异常,可以使用通用异常处理程序处理多个异常。
如果需要的话,我可以自己制作轻量级的“异常”类型。那么为什么要费心使用std::exception呢?如果您需要自定义异常类,请自行创建。但是std::exception使您的工作更加轻松,因为它已经提供了许多良好的异常类应该具有的功能。它为您提供了从中派生和覆盖必要函数(特别是std::exception::what())以实现类功能的便利。
这给您带来了两个优势:std::exception处理程序,
  • 既可以捕获标准库异常,也可以捕获您自定义异常类的异常

图片来源:http://en.cppreference.com/w/cpp/error/exception


1
我会认为what并不是一个好的函数的例子。我对它返回C字符串的事实表示异议。天哪,都已经2013年了! - Matthieu M.

6

C++标准为何会费心发明std::exception类?有什么好处吗?

拥有不同的异常类型允许你捕捉特定类型的错误。从一个共同的基类派生异常允许在捕获更通用或特定的错误时进行粒度控制。

在C++中,已经存在一个现有的类型系统,因此在语言中可以显式创建所需类型的异常时,标准化错误字符串是不必要的。

std::exception 及其派生类存在的两个主要原因:

  1. 标准库必须具有某种异常层次结构以在异常情况下抛出。始终抛出std::string是不合适的,因为您没有清晰的方式来针对特定类型的错误。

  2. 为库供应商提供可扩展的基于类的接口,以抛出最基本的错误类型并为用户提供通用回退。您可能希望提供比简单的what()字符串更多的错误元数据,以便捕获您的错误的人可以更智能地从中恢复。

    同时,std::exception作为一个公共基础允许一个常规的catchall,不像...那样包罗万象,如果用户只关心该错误消息。

如果你所做的一切都是打印并退出,那么这并不重要,但你可以使用从std::exception继承的std::runtime_error以方便捕获。

稍后,如果需要,我可以自己制作轻量级的“异常”类型。那么我为什么要费心去处理std::exception呢?

如果您从 std::runtime_error 继承并使用自定义错误类型,那么您可以在不重写 catch 块的情况下添加错误元数据!相反,如果您改变了错误处理设计,则必须重写所有的 std::string catches,因为您不能安全地从 std::string 继承。这不是一项前瞻性的设计决策。
如果现在看起来还不太糟糕,想象一下您的代码成为共享库,并在多个项目中共享,有各种程序员在上面工作。迁移到新版本的库将变得非常麻烦。
这甚至没有提到 std::string 可能在复制、构造或访问字符时抛出自己的异常!
Boost 的网站在异常处理和类构造方面有一些很好的指南 here
用户故事
我正在编写一些网络代码,并使用第三方供应商的库。当用户输入无效的IP地址时,该库会抛出一个自定义异常nw::invalid_ip,它派生自std::runtime_errornw::invalid_ip包含一个描述错误消息的what(),以及提供的incorrect_ip()地址。
我还使用std::vector来存储套接字,并使用检查过的at()调用来安全地访问索引。我知道如果我在超出范围的值上调用at(),则会抛出std::out_of_range
我知道其他事情也可能被抛出,但我不知道如何处理它们,或者它们到底是什么。
当我遇到nw::invalid_ip错误时,我会弹出一个模态框,其中包含一个输入框,用户可以编辑并重试。
对于std::out_of_range问题,我通过对套接字进行完整性检查并修复已经失去同步的向量/套接字关系来响应。
对于任何其他std::exception问题,我将以错误日志终止程序。最后,我有一个catch(...),记录“未知错误!”并终止。
只使用std::string抛出异常将很难做到这一点。
这是一个基本示例,演示了不同情况下抛出异常的几个要点,以便您可以尝试捕获异常。

ExampleExceptions.cpp

#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?

2
这是一个合理的问题,因为`std::exception`实际上只包含一个属性:`what()`,一个字符串。所以很容易就想到使用字符串而不是异常。但事实上,异常不是字符串。如果你把异常当作仅仅是字符串来处理,你将失去在特定异常类中派生更多属性的能力。
例如,今天你在自己的代码中抛出字符串。明天你决定为某些情况添加更多属性,比如数据库连接异常。你不能仅仅从字符串派生这个变化;你需要编写一个新的异常类,并更改所有针对字符串的异常处理程序。使用异常是一种让异常处理程序仅使用他们关心的数据、根据需要选择异常来处理的方式。
此外,如果你仅抛出和处理字符串类型的异常,你将错过任何不属于你自己代码的地方抛出的异常。如果这种区别是有意的,最好使用一个通用的异常类来表示,而不是使用泛型类型的字符串。
异常比字符串更具体。这意味着库开发人员可以编写接受异常作为参数的函数,这比接受字符串更清晰。
所有这些本质上都是免费的,只需使用异常而不是字符串。

1

仅仅因为一个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&来捕获它们,而不需要知道(或关心)具体的类型。

0
如果你是类的唯一用户,你可以避免使用 std::exception如果你想避免使用标准库异常)。

但是如果你的类将被其他人(程序员)使用,他们如何处理异常呢?

如果你的类throws一个描述错误string,那么这并没有什么帮助,因为你的类的消费者更喜欢一种更标准的方式(捕获异常对象并查询它的方法)来处理exception,而不是捕获一个字符串。

此外,您可以通过捕获exception对象来捕获标准库抛出的异常。


您可以重写exception类的what方法,以提供有关错误的更多信息。


0
哇,我很惊讶没有人提到这一点:
  1. 你需要多种类型的异常才能够区分它们 -- 有些异常应该被处理,而其他异常则不需要。

  2. 它们需要有一个共同的基类,以便为用户提供显示合理信息的手段,而不必知道程序可能抛出的所有异常类型(使用外部闭源库时是不可能的)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接