什么是特殊异常类的需要?

时间:2013-01-19 13:42:08

标签: c++ exception stl hierarchy

为什么C ++标准难以发明std::exception类?他们的好处是什么?我的理由是:

try
{
  throw std::string("boom");
}
catch (std::string str)
{
  std::cout << str << std::endl;
}

工作正常。后来,如果我需要,我可以制作自己的轻量级“异常”类型。那么我为什么要打扰std::exception

6 个答案:

答案 0 :(得分: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处理程序

提供了2个优势
  • 可以捕获标准库异常以及
  • 自定义异常类的类型的例外

图片提供:http://en.cppreference.com/w/cpp/error/exception

答案 1 :(得分:6)

  

为什么C ++标准难以发明std :: exception类?他们的好处是什么?

使用不同的kinds of exceptions可以捕获特定的类型错误。从公共基础派生异常允许根据环境捕获更多通用或特定错误的粒度。

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

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

  1. 标准库必须具有某种异常层次结构 扔在特殊情况下。这是不合适的 总是抛出 std :: string ,因为你没有干净的方法 针对特定类型的错误。

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

    同时 std :: exception 作为一个共同基础,如果用户只关心该错误消息,则允许一般的捕获而不是 ...

  3. 如果你所做的只是打印并退出那么它并不重要,但你也可以使用继承自 std :: exception std :: runtime_error >为方便起见。

      

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

    如果您继承自 std :: runtime_error 并使用自己的自定义错误类型,那么您可以追溯性地添加错误元数据,而无需重写catch块!相反,如果您更改了错误处理设计,那么您将被迫重写所有 std :: string 捕获,因为您无法安全地从 std :: string 继承。这不是一个前瞻性的设计决定。

    如果现在看起来不那么糟糕,想象一下,如果您的代码在多个项目中作为共享库共享,并且各种程序员都在使用它。迁移到新版本的库会很麻烦。

    这甚至没有提到std :: string在复制,构造或访问字符时可以抛出自己的异常!

    Boost的网站在异常处理和类构建方面有一些很好的指导here

    用户故事

      

    我正在编写一些网络代码并使用第三方供应商   图书馆。在用户输入的无效IP地址上   库抛出派生自的自定义异常 nw :: invalid_ip   的的std :: runtime_error 即可。 nw :: invalid_ip 包含 what()描述   错误消息,以及提供的 incorrect_ip()地址。

         

    我还使用 std :: vector 来存储套接字,并且正在使用   选中 at()调用以安全地访问索引。我知道,如果我   在 std :: out_of_range 超出范围的值上调用 at()

         

    我知道其他事情也可能被抛出,但我不知道该怎么做   处理它们,或它们究竟是什么。

         

    当我收到 nw :: invalid_ip 错误时,我弹出一个带输入的模态   填充了无效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 :(得分:2)

这是一个合理的问题,因为std::exception只包含一个属性:what()string。因此,使用string代替exception很有吸引力。但事实是exception不是string。如果您将异常视为仅仅string,则您将无法在提供更多属性的专门异常类中从中派生它。

例如,今天你在自己的代码中抛出string。明天你决定在某些情况下添加更多属性,比如数据库连接异常。你不能只从string派生出来进行这种改变;您需要编写一个新的异常类并更改string的所有异常处理程序。使用exception是异常处理程序仅使用它们关心的数据的一种方法,可以在需要处理它们时选择异常。

此外,如果您只抛出并处理string个类型的异常,您将错过任何不属于您自己的代码抛出的所有异常。如果这种区分是有意的,最好通过使用公共异常类来表示,而不是通用类型string

exception也比string更具体。这意味着库开发人员可以编写接受异常作为参数的函数,这比接受string更清晰。

所有这些都是免费的,只需使用exception代替string

答案 3 :(得分: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&捕获它们。

答案 4 :(得分:0)

如果您是该班级的唯一用户,则可以避免std::exception如果您想避免标准库例外


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

如果您的课程throws string描述了错误,那将无济于事,因为您班级的消费者会更喜欢标准 way(捕获异常对象并查询它是什么方法)来处理exception而不是捕获字符串..

您还可以通过捕获exception对象

来捕获标准库抛出的异常

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

答案 5 :(得分:0)

哇,我很惊讶没有人提到这个:

  1. 您需要多种类型的例外才能区分它们 - 应该处理某些类型的例外,而其他例外则不应该处理。

  2. 他们需要有一个通用的基类,以便您有一种方法向用户显示理智的消息,而无需知道所有您的程序可能的异常类型throw(使用外部封闭源库时不可能)。