【问题标题】:Distinguishing between multiple exceptions of the same type区分同一类型的多个例外
【发布时间】:2015-10-26 13:10:53
【问题描述】:

我无法完全理解用户将如何区分我的函数可能引发的异常。我的一个函数可以抛出std::invalid_argument 的两个实例。

例如,在构造函数中:

#include <stdexcept> // std::invalid_argument
#include <string>

class Foo
{
public:
    void Foo(int hour, int minute)
    :h(hour), m(minute)
    {
        if(hour < 0 || hour > 23)
            throw std::invalid_argument(std::string("..."));
        if(minute < 0 || minute > 59)
            throw std::invalid_argument(std::string("..."));
    }
}

注意:这是一个例子,请不要用有界整数回答。

假设用户调用foo(23, 62);,用户的异常处理程序将如何区分std::invalid_argument 的两个可能实例?

还是我做错了,我应该从 std::invalid_argument 继承来区分它们?也就是说,

class InvalidHour: public std::invalid_argument
{
public:
    InvalidHour(const std::string& what_arg)
    :std::invalid_argument(msg) {};
}

class InvalidMinute: public std::invalid_argument
{
public:
    InvalidMinute(const std::string& what_arg)
    :std::invalid_argument(msg) {};
}

然后,抛出InvalidHourInvalidMinute

编辑:为每个可能的异常创建一个类对我来说似乎有点过分,尤其是在大型程序中。是否每个以这种方式有效使用异常的程序都附带有关于捕获什么的大量文档?

正如答案中提到的,我考虑过assert。环顾stackoverflow,我发现大多数人说你应该抛出异常(因为我的特殊情况是针对构造函数)。

看了很多关于何时使用异常的在线信息后,普遍的共识是使用assert 处理逻辑错误和异常处理运行时错误。虽然,使用无效参数调用 foo(int, int)可能是运行时错误。这就是我要解决的问题。

【问题讨论】:

  • 理想情况下你应该有两种类型,HourMinute,当使用无效值构造时它们会抛出它们自己的异常类型。
  • 是的,你是对的,如果你想以一种简洁的方式区分它们,你应该引入新的类型
  • “用户如何区分我的函数可能抛出的异常” 为什么你需要用户/用户需要区分这两种情况? foo 是否只验证其参数?
  • 不应该是hour &gt; 23吗?还是您想要 25 个不同的可能时间?
  • @fredoverflow:我认为在某些系统中,24h00 有效(24h30 无效)。所以还有第三种情况,当两个参数的组合无效时。 (很难找到一个正确/明确/简单的名称)。

标签: c++ c++11 exception exception-handling


【解决方案1】:

标准异常层次结构不适用于逻辑错误。使用assert 并完成它。如果您确实希望将硬错误转换为更难检测运行时错误,请注意处理程序只能做两件合理的事情:以某种可能不同的方式实现合同目标(可能只是重试操作),或者在转而抛出异常(通常只是重新抛出),并且原始异常的确切原因很少在其中发挥任何作用。最后,如果您确实想要支持真正尝试各种参数组合的代码,直到它找到一个不会抛出的代码,不管它现在用文字写出来看起来多么愚蠢,好吧,你有 std::system_error 用于传递一个整数错误代码,但您可以定义派生异常类。

说了这么多,去assert吧。

这就是它的用途。

【讨论】:

  • 如果结果是运行时错误怎么办?在那种情况下终止是不合理的,不是吗?
  • @ShreyasVinod:术语“运行时错误”通常指的是运行时的一些故障处理,但不可能是无效的参数值。因此,对于您的意思,我只有一个模糊的可能性,但我认为无论它是什么,将函数视为具有合同都会有所帮助。它有一些要求,例如在参数值上,如果满足这些要求,则有义务产生特定结果,或者如果失败,则抛出异常。否则,如果违反合同,保修将失效。这就像汽车发动机:给它加油,好,给它糖水,!。
  • 我的想法更像是用户无法控制的事情。也许最终用户提供的外部值恰好是错误的。我想在将其传递给我的函数之前确保此输入有效是函数用户的责任。你完全正确。
【解决方案2】:

您还可以创建更多派生自 invalid_argument 的错误类,这将使它们可区分,但这不是可扩展的解决方案。如果您真正想要的是向用户显示他可以理解的消息,那么 invalid_argument 的字符串参数将用于此目的。

【讨论】:

    【解决方案3】:

    标准异常不允许存储您想要的附加信息,并且解析异常消息是一个坏主意。正如您所提到的,一种解决方案是子类化。还有其他的 - 随着std::exception_ptr 的出现。可以像在 Java 或 .NET 中一样使用“内部”(或“嵌套”)异常,尽管此功能更适用于异常转换。有些人更喜欢 Boost.Exception,作为可在运行时扩展的异常的另一种解决方案。

    不要像 Cheers 和 hth 那样落入“只是断言陷阱”。简单例子:

    void safe_copy(const char *from, std::size_t fromLen, char *buf, std::size_t bufLen)
    {
        assert( fromLen <= bufLen );
        std::copy(from, from + fromLen, buf);
    }
    

    assert 本身并没有什么问题,但是如果代码是为发布而编译的(设置了NDEBUG),那么safe_copy 将根本不安全,结果可能是缓冲区溢出,可能允许恶意方接管该过程。如前所述,抛出异常来指示逻辑错误有其自身的问题,但至少它会阻止发布版本中立即出现未定义的行为。因此,我建议,在安全关键功能中,在调试中使用断言,在发布版本中使用异常:

    void safe_copy(const char *from, std::size_t fromLen, char *buf, std::size_t bufLen)
    {
        assert( fromLen <= bufLen );
        if ( fromLen > bufLen )
            throw std::invalid_argument("safe_copy: fromLen greater than bufLen");
        std::copy(from, from + fromLen, buf);
    }
    

    当然,如果你经常使用这种模式,你可能希望自己定义一个宏来简化任务。但是,这超出了当前主题的范围。

    【讨论】:

    • 我曾经(就像 15 年前一样)总结了您提出的解决方案,一个带有断言和异常抛出的组合,并在网络上发布了一篇关于它的文章(今天将是一篇博客文章)。稍微不安全一点,传播到顶部的硬异常,代替assert的异常不应该是标准异常。原来我后来没用过那个东西。其他人也没有。这就像定义一个WITH 宏的想法(是的也是BTDT)。所以我认为你驳回我看似简单的建议有点为时过早。它基于您曾经发明的解决方案。
    • 嗯,虽然我在这里,但我最好注意,不检查无符号整数参数的巨大值的参数有效性检查比不检查更糟糕。在不检查最常见的错误(负值环绕)的同时给出错误的安全感。如果您绝对想要 64 位构建的可能较大的 64 位范围,并且想要一个描述性名称,那么using Size = ptrdiff_t 是您的朋友。
    【解决方案4】:

    抛出异常而不是断言的另外两个原因是,当您正在实现一个库或某种形式的可导出代码并且无法告诉先验用户将如何处理某种形式的错误或何时您需要检查用户何时以 RELEASE 模式构建您的代码(用户经常这样做)。请注意,在 RELEASE 模式下构建会“带走”任何断言。

    例如,看看这段代码:

    struct Node 
    { 
        int data; 
        Node* next;
        Node(int d) : data(d), next(nullptr) {}
    };
    
    // some code 
    Node* n =  new Node(5);
    assert(n && "Nodes can't be null");
    
    // use n
    

    当此代码以 RELEASE 模式构建时,该断言“不存在”并且调用者可能会在运行时将 n 变为 nullptr

    如果代码抛出异常而不是断言,调用者仍然可以在调试和发布版本中对nullptr 异常“做出反应”。缺点是异常方法需要更多样板代码。

    【讨论】:

      猜你喜欢
      • 2013-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-01-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多