2012-02-22 39 views
27

两种反模式在大多数代码库中非常常见,我已经使用布尔返回值来指示成功/失败,以及通用的整型返回码来指示有关错误消息的更多详细信息。如何在C++中设计异常“类型”?

这两者都非常类似C,并且在我的愚见中不太适合C++。

我的问题是关于最佳实践,当涉及到设计异常到您的代码库。换句话说,指出失败的有限可能性的最佳方式是什么?例如,前面提到的反模式之一通常会有一个巨大的枚举,每个枚举值代表一种特定类型的故障,例如FILE_DOES_NOT_EXISTNO_PERMISSIONS。通常情况下,它们尽可能保持一般,以便可以跨多个不相关的域(如网络组件和文件IO组件)使用它们。

与此类似的设计,人们可能会考虑异常的设计是从std::exception中为每种类型的失败或可能出错的事物子类化一个具体异常类型。所以在我前面的例子中,我们有以下几点:

namespace exceptions { 
    class file_does_not_exist : public std::exception {}; 
    class no_permissions : public std::exception {}; 
} 

我觉得这是更接近的东西,“感觉更好”,但最终这似乎只是一个维护的噩梦,特别是如果你有上百这些“错误代码”可以翻译成类。

我见过的另一种方法是简单地使用标准的<stdexcept>类,例如std::runtime_error,并有一个具有特定字符串。例如:

throw std::runtime_error("file does not exist"); 
throw std::runtime_error("no permissions"); 

这样的设计更容易维护,但让人难以或不可行有条件地赶上这些异常应该他们都将有可能从相同的核心位置或函数调用抛出。

那么什么是一个好的,可维护的异常类型设计?我的要求很简单。我想要了解发生了什么情况的上下文信息(是否内存不足?我缺少文件系统权限吗?我没有满足函数调用的前提条件(例如错误的参数)吗?),我也想以便能够相应地处理这些信息。也许我对待他们都是一样的,也许我有特定的失败声明,所以我可以从他们不同的方式恢复。

我对这个问题的研究才使我这样一个问题: c++ Exception Class Design

用户在这里问,我是一个类似的问题,他在底部代码示例几乎是惹人喜爱,但他的基本异常类做不遵循开放/封闭的原则,所以这对我来说真的不起作用。

+1

什么方面这个问题从链接的问题区分开来,除了与答案模糊的不适? – 2013-06-02 19:38:37

回答

26

C++标准库’的异常层次结构是恕我直言,相当任意和毫无意义。例如,如果任何人开始真正使用例如,它可能会产生问题。 std::logic_error而不是终止,当它’明确该程序有一个非常讨厌的错误™。正如标准所述,

“逻辑错误的显着特点是它们是由于程序内部逻辑错误造成的。 ”

因此,在地步,否则可能似乎是合理扔std::logic_error程序的状态可能会被不可预知搞砸了,并继续执行,可能会让用户’ s的数据处于危险’小号方式。

尽管如此,像std::string标准的异常类层次结构有一个非常非常重要的现实意义和实用的功能,即它’小号正式标准

因此,任何自定义的异常类应该间接推导出来,或者直接从std::exception得到(尽管我不推荐它)。

通常,当有关自定义异常类的辩论肆虐十年前,我建议只能从std::runtime_error推导和我仍然建议。它是支持自定义消息的标准异常类(其他消息通常具有硬编码消息,因为它们有可识别的价值,所以最好不要改变它们)。和一个可能认为std::runtime_error是标准的异常类表示可恢复故障(相对于不可恢复逻辑错误,其可以’吨被固定在运行时),或者作为标准所说的那样,

“运行时错误是由于超出程序范围的事件造成的。他们不能容易地提前预测”。

有时被用于其他事情的C++异常机制,正如刚刚一个低级别的动态目标跳跃机制处理。例如,聪明的代码可以使用异常来传递递归调用链中的成功结果。但异常失败是最常见的用法,并且’是C++异常通常优化的用途,所以大多数情况下,使用std::runtime_error作为任何自定义异常类层次结构的根–是合理的,即使这迫使某个想要成为聪明,抛出“失败” - 指示异常表示成功&hellip;

值得注意的是:也有std::runtime_error和三个标准的子类,即std::range_errorstd::overflow_errorstd::underflow_error,而相反的是他们的名字表示不需要后两者要由浮点运算生成,并产生不实际通过浮点运算,但都是AFAIK只生成了一些–的惊喜! – std::bitset操作。简而言之,标准库’的异常类层次结构在我看来似乎已经被抛入其中,只是为了出现’的缘故,没有任何真正的好理由或现有的做法,甚至没有做一个无意义的检查。但是,也许我错过了这一点,如果是的话,那么我仍然有新的东西来了解这一点。 :-)

那么,std::runtime_error那么。

在自定义异常类的层次结构的顶部,用C++ 03它是由C++标准的03例外缺少重要的东西添加有用:

  • 虚拟clone方法(尤其是通过C代码传递异常很重要)。

  • 虚拟throwSelf方法(与克隆相同的主要原因)。

  • 支持链式异常消息(标准化格式)。

  • 支持携带失败原因代码(如Windows或Posix错误代码)。

  • 支持从携带的失败原因代码中获取标准消息。

C++ 11增加了对多本,但除了尝试失败的原因码和消息中新的支持,并指出,不幸的是它’小号漂亮Unix的具体不是很适合的Windows ,我没有使用’。无论如何,为了完整性:不要添加克隆和虚拟重新抛出(这是普通应用程序员在自定义异常类层次结构中可以做到的最好的方式,因为作为应用程序员,您不能将当前异常对象从实现的存储中提取出来C++ 11标准增加了免费函数std::current_exception()std::rethrow_exception(),而不是支持链接的异常消息,它增加了一个mixin类std::nested_exception和免费函数std::rethrow_nestedstd::rethrow_if_nested

考虑到对上述要点的部分C++ 11支持,新的和现代的自定义异常类层次结构应该更好地与C++ 11支持集成,而不是解决C++ 03的缺点。那么,除了C++ 11的失败代码的东西,这似乎是非常不适合Windows编程。因此,在std::runtime_error的自定义层次结构的顶部,理想情况下至少有一个通用异常类,并从中派生出一个支持传播失败代码的异常类。

现在,最后是问题的要点:现在最好是为每个可能的故障原因派生出一个唯一的异常类,或者至少对于重大故障原因?

我会说:DON ’ T添加无需复杂性

如果调用者能够识别某个故障原因,或者该调用者可以识别某个故障原因,那么为此提供一个不同的异常类是非常有用的。但在大多数情况下,调用者感兴趣的唯一信息就是发生异常的唯一事实。不同的故障原因导致不同的尝试修复是非常罕见的。

但是失败原因码呢?

那么,当这是一个底层的API给你时,它只是添加工作来创建相应的异常类。但在另一方面,当你在调用链通信失败了,来电者可能需要知道确切原因,然后使用一个代码意味着调用者将不得不使用catch里面的一些嵌套检查和调度。所以这些是不同的情况:(A)你的代码是失败指示的原始来源,而(B)你的代码使用例如Windows或Posix API函数失败,并且通过失败原因代码指示失败原因。

+0

优秀的信息,虽然我对克隆/自我筛选的东西有点困惑你谈到了。你说'你不能挂起当前异常对象进行存储的实现的异常传播uses'它并没有真正任何意义,我的。你能解释一下我的一些很好的阅读材料吗? – 2012-02-22 18:52:51

+0

@RobertDailey:作为一个例子,在C++ 11的标准建议在§18.8.5.6该实现可以使用一个'shared_ptr'或如'exception_ptr'相似。然后必须动态分配指向的异常对象。对于例如'函数current_exception()'工作,当前存在传播的异常的内部存储也必须动态分配的,以便它可以由智能指针结果被参考。但*你*不知道需要什么样的破坏,所以你不能实现你自己的'current_exception()'。而在C++ 03中则更糟。 – 2012-02-23 01:50:40

+0

我从来没有使用'exception_ptr'或'current_exception()'。我总是做了一些像'throw MyException();'的事情。为什么我会用这两个项目呢? – 2012-03-01 17:07:24

4

我已经使用boost::exception一段时间了,我真的很喜欢将arbitrary data插入一个异常。 除了特定的异常类型外,我还会这样做。

#define MY_THROW(x) \ 
    BOOST_THROW_EXCEPTION(x << errinfo_thread_id(boost::this_thread::get_id())) 

class DatabaseException : public std::exception, public boost::exception { ... }; 

typedef boost::error_info< struct errinfo_message_, std::string > errinfo_message; 

MY_THROW(DatabaseException(databaseHandle) 
     << boost::errinfo_api_function("somefunction") 
     << errinfo_message("somefunction failed terribly.") 
     ); 

这种方式可以捕获特定的异常,同时也从废弃的网站提供详细的负载(例如,文件名,行号,线程ID,...)。

它还提供了一些pretty printing的异常消息及其细节。 大多数情况下,我在日志中写入该信息并中止程序,具体取决于例外情况。

编辑:正如你所引用的线程指出,使用浅的层次结构。我使用了类似于3-4个异常类的东西,它们直接从std :: exception和boost :: exception继承。我还将大量细节放入例外中(例如线程ID)。

+1

我倾向于选择一个更平坦的hieararchy现在。我们的项目只是使用“关键”和“可恢复”的例外。前者基本上表示不可恢复的情况,我们可以做的最好的是发出调试信息。后者当然可以恢复,所有代码都可以这样做。它使它非常简单。 – 2012-02-22 07:46:44

+0

虽然我不能说我赞成在这里使用宏。 – 2012-03-01 17:31:08

+2

@ EDA-qamort-ORA-Y:是不是真的到抛出现场决定的错误是可恢复的还是不是? – 2013-04-15 13:41:57

0

如果您的自定义异常指示编程错误(即程序员可能阻止了该条件),则从logic_error派生。这些错误可能无法恢复,因为程序员可能没有预料到它的位置。例如,有人试图用0

class divide_by_zero : public logic_error { 
public: 
    divide_by_zero(const string& message) 
     : logic_error(message) { 
    } 
} 

如果错误状况是不可能被阻止由程序员然后从runtime_error派生的东西来划分。通常希望这些错误是可恢复的(即程序员可以捕获异常并重新尝试或继续)。

class network_down : public runtime_error { 
public: 
    network_down(const string& message) 
     : runtime_error(message) { 
    } 
} 

这也是在标准库中设计例外的一般原理。您可以查看exception code for GCC here