区分相同类型的多个异常

5

我有些困惑用户如何能够区分我的函数可能抛出的异常。我的一个函数可以抛出两个 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) 可能是运行时错误。这就是我想要解决的问题。


4
理想情况下,你应该有两种类型,HourMinute,当使用无效值构造它们时,它们会抛出自己的异常类型。 - user657267
是的,你说得对,如果你想以整洁的方式区分它们,你应该引入新的类型。 - Maksim Solovjov
3
用户如何区分我函数可能抛出的异常。为什么需要用户/用户需要区分这两种情况?foo 只验证其参数吗? - dyp
2
那不应该是 hour > 23 吗?还是你想要25个不同的小时? - fredoverflow
@dyp 旨在作为构造函数。已将问题更新为 Foo 作为一个类。 - Shreyas
显示剩余2条评论
4个回答

4
标准异常层次结构不适用于逻辑错误。使用assert,然后就可以了。如果您一定要将难以解决的错误转换为更难检测的运行时错误,请注意处理程序只有两件合理的事情可做:以某种可能不同的方式实现合同目标(可能仅是重试操作),或者反过来抛出异常(通常只是重新抛出)。原始异常的确切原因在这中间很少扮演任何角色。最后,如果您确实想支持尝试各种参数组合直到找到一个不会抛出异常的代码,无论现在写成文字看起来多么愚蠢,都可以使用std::system_error将整数错误代码上传,但您可以定义派生异常类。
总之,使用assert即可。

如果最终结果是运行时错误怎么办?在这种情况下,终止操作是不合理的,对吧? - Shreyas
“运行时错误”一词通常指运行时的某些故障处理,但无效的参数值不可能是这样。因此,我对您的意思只有一个模糊的可能性,但我认为无论是什么,都可以将函数视为具有“合同”的形式来帮助解决问题。它有一些要求,例如对参数值的要求,如果满足这些要求,则必须产生某个特定的结果或者在失败时抛出异常。否则,如果违反了合同,则保修将无效。这就像汽车发动机:加油没问题,加糖水就不行了。 - Cheers and hth. - Alf
我认为更像是用户无法控制的某些事情。也许是最终用户提供的外部值,恰好是错误的。我猜这个函数的用户有责任确保在传递给我的函数之前输入是有效的。你完全正确。 - Shreyas

1
你还可以创建更多的错误类,它们派生自invalid_argument,这样它们就可以区分开来,但这不是一种可扩展的解决方案。如果你实际想要的是向用户显示一个可理解的消息,那么传递给invalid_argument的字符串参数就可以达到这个目的。

1
标准异常不允许存储所需的额外信息,解析异常消息也是不好的想法。一种解决方案是子类化,正如您提到的那样。还有其他解决方案-随着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宏的想法一样(是的,我也做过)。因此,我认为你对我的显然简单的建议的否决有些过早。这是基于我曾经发明你的解决方案。 - Cheers and hth. - Alf
1
嗯,顺便提一下,不检查无符号整数参数的巨大值的参数有效性检查比没有检查更糟糕。这会给人一种虚假的安全感,同时又没有检查最常见的错误(负值的环绕)。如果您绝对想要64位构建的可能很大的64位范围,并且您想要一个描述性名称,那么“using Size = ptrdiff_t”就是您的朋友。 - Cheers and hth. - Alf

1
两个使用异常而不是断言的原因是当您正在实现库或某种可导出代码并且无法在先知道用户将如何处理某种形式的错误时,以及当用户在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模式下编译时,该断言“不存在”,调用者可能会在运行时得到nnullptr
如果代码抛出异常而不是断言,调用者仍然可以在调试和发布版本中对nullptr异常进行“反应”。缺点是异常方法需要更多的样板代码。

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