有很多地方可以找到设计异常类的指南。几乎在我看的每个地方,都有异常对象永远不应该做的事情列表,这会影响这些类的设计。
例如,该类不包含std::string
成员的Boost people recommend,因为它们的构造函数可能会抛出,这会导致运行时立即终止程序。
现在,在我看来,这是理论上的。如果std::string
的构造函数抛出,则它是一个bug(我传入一个空指针)或一个内存不足的情况(如果我错了,请纠正我)。由于我在桌面上,我只是假装我有无限的内存,内存耗尽对我的应用程序来说是致命的,无论。
考虑到这一点,为什么我不应该在我的异常类中嵌入std::string
个对象?事实上,为什么我的异常类不能全功能,并且还要处理日志记录,堆栈跟踪等。我知道一个责任原则,在我看来,这是一个公平的权衡让异常类完成所有这些。当然,如果我的解析器需要报告语法错误,那么功能齐全的异常将比围绕静态分配的字符数组构建的异常更有帮助。
所以:精益C ++异常类 - 它在现实世界中有多大的优势?有什么权衡取舍?是否有关于该主题的良好讨论?
答案 0 :(得分:3)
您可以使用Boost.Exception库来帮助定义异常层次结构。 Boost.Exception库支持:
将任意数据传输到 抓住网站,这是非常棘手的 由于无投掷要求 (15.5.1)异常类型。
框架的局限性将为您提供合理定义的设计参数。
答案 1 :(得分:2)
作为一般情况,异常类应该是简单的,自给自足的结构,永远不会分配内存(如std::string
那样)。第一个原因是分配或其他复杂操作可能会失败或产生副作用。另一个原因是异常对象按值传递,因此是堆栈分配的,因此它们必须尽可能轻量级。更高级别的功能应该由客户端代码处理,而不是异常类本身(除非出于调试目的)。
答案 2 :(得分:2)
在允许代码处理异常的任何考虑之前,异常的头号工作是能够向用户和/或开发人员报告确切的错误。一个异常类,它不能报告OOM,但只是崩溃程序而不提供任何线索,为什么它崩溃是不值得的。 OOM现在变得非常普遍,32位虚拟内存耗尽了气体。
向异常类添加大量辅助方法的麻烦在于它会强制您进入您不一定想要或不需要的类层次结构。现在需要从std :: exception派生,因此您可以使用std :: bad_alloc执行某些操作。当您使用具有不是从std :: exception派生的异常类的库时,您将遇到麻烦。
答案 3 :(得分:2)
看看他们在内部使用std :: string的std例外 (或者我应该说我的g ++实现确实如此,我确信标准在这个问题上没有提及)
/** Runtime errors represent problems outside the scope of a program;
* they cannot be easily predicted and can generally only be caught as
* the program executes.
* @brief One of two subclasses of exception.
*/
class runtime_error : public exception
{
string _M_msg;
public:
/** Takes a character string describing the error. */
explicit runtime_error(const string& __arg);
virtual ~runtime_error() throw();
/** Returns a C-style character string describing the general cause of
* the current error (the same string passed to the ctor). */
virtual const char* what() const throw();
};
我通常从runtime_error(或其他标准异常之一)派生我的异常。
答案 4 :(得分:2)
由于我在桌面上,我只是假装我有无限的内存,无论如何,内存耗尽对我的应用程序来说是致命的。
因此,当您的应用程序致命失败时,您不希望它干净地终止吗?让析构函数运行,文件缓冲区或日志被刷新,甚至可能向用户显示错误消息(甚至更好,错误报告屏幕)?
考虑到这一点,为什么我不应该在我的异常类中嵌入std :: string对象?事实上,为什么我的异常类不能全功能,并且还要处理日志记录,堆栈跟踪等。我知道一个责任原则,在我看来,这是一个公平的权衡让异常类完成所有这些。
为什么这是公平的权衡?为什么在所有中进行权衡?权衡意味着您对单一责任原则做出了一些让步,但据我所知,您不会这样做。你只是说“我的例外应该做的一切”。这几乎不是一种权衡。
与SRP一样,答案应该是显而易见的:通过使异常类完成所有工作,您获得了什么?为什么记录器不能成为一个单独的类?为什么必须由例外执行?不应该由异常处理程序处理吗?您可能还希望进行本地化,并提供不同语言的语法错误消息。因此,您的异常类应该在构造时出去读取外部资源文件,寻找正确的本地化字符串?这当然意味着另一个潜在的错误来源(如果找不到字符串),会增加异常的复杂性,并且需要异常才能知道其他无关信息(用户使用哪种语言和区域设置)。格式化的错误消息可能取决于它的显示方式。也许它在记录时,在消息框中显示或打印到stdout时应格式不同。处理异常类的问题更多。还有更多可能出错的事情,更多可能发生错误的代码。
您的异常尝试越多,出错的可能性就越多。如果它尝试记录,那么如果磁盘空间不足会发生什么?也许你也假设无限的磁盘空间,只是忽略它,如果它发生,你将丢弃所有的错误信息? 如果您没有日志文件的写权限,该怎么办?
根据经验,我不得不说,有些事情比没有获得有关刚刚发生的错误的任何信息更令人讨厌,因为发生了错误。如果您的错误处理无法处理错误发生,那么它实际上并不是错误处理。如果异常类无法处理被创建和抛出而不会导致更多异常,有什么意义?
通常,SRP的原因是您添加到类中的复杂性越高,确保正确性就越困难,并且理解代码。这仍然适用于异常类,但您还会得到第二个问题:您添加到异常类的复杂性越高,发生错误的机会就越多。通常,您不希望在抛出异常时发生错误。毕竟,你已经处理了另一个错误。
但是,“异常类不应包含std::string
的规则与”不允许异常类分配内存“的规则并不完全相同。std::exception
执行后者。毕竟存储一个C风格的字符串.Boost只是说不存储可能抛出异常的对象。所以如果你分配内存,你只需要能够处理分配失败的情况。
当然,如果我的解析器需要报告语法错误,那么功能齐全的异常将比围绕静态分配的字符数组构建的异常更有帮助。
说刚才说他不介意应用程序的人只是在发生错误时没有向用户反馈而终止。 ;)
是的,您的异常应包含生成友好,可读的错误消息所需的所有数据。在解析器的情况下,我会说必须是这样的:
根据这些信息,您可以在处理错误时为用户生成一个友好,健壮的错误消息。如果您愿意,甚至可以将其本地化。您可以在处理异常时将其本地化。
通常,异常类供程序员使用。它们不应包含或构建针对用户的文本。正确创建可能很复杂,应该在处理错误时完成。
答案 5 :(得分:1)
C ++标准要求异常具有无抛出复制结构。如果您有一个std :: string成员,则没有无抛出的复制构造函数。如果系统无法复制您的例外,它将终止您的程序。
在设计异常类型层次结构时使用虚拟继承也是一个好主意,如http://www.boost.org/doc/libs/release/libs/exception/doc/using_virtual_inheritance_in_exception_types.html中所述。
但是,不要求异常对象很简单或不分配内存。事实上,异常对象本身通常在堆上分配,因此系统可能会耗尽内存以试图抛出异常。
答案 6 :(得分:0)
我认为拒绝在异常类中使用std :: string是不必要的纯粹主义。是的,它可以抛出。所以呢?如果你的std :: string实现因为你正在构造一条消息“无法解析文件Foo”而抛出而出于内存耗尽,那么实现有问题,而不是用你的代码。
对于内存不足,即使构造不带字符串参数的异常,也会出现此问题。添加20个字节的有用错误消息不太可能造成或破坏。在桌面应用程序中,当您尝试错误地分配20 GB的内存时,大多数OOM错误都会发生,这不是因为您一直很高兴地以99.9999%的容量运行,并且有些东西让您超过顶部。
答案 7 :(得分:0)
......无论如何,内存耗尽对我的应用程序来说是致命的。
不幸的是,这正是大多数人所说的,因为他们不想处理其他情况下出现的复杂性。另一方面,如果您遵循标准设置的要求,您将获得更强大的软件,即使在低内存条件下也可以恢复。
将火星车停在一边,想想像编写文本处理器这样简单的情况。您想要实现复制/粘贴功能。用户选择一些文本并按Ctrl + C.你喜欢什么结果:崩溃或消息“内存不足”? (如果没有足够的内存来显示消息框,则没有任何反应。)第二种情况无疑更加用户友好,她可以关闭其他应用程序并继续处理她的文档。
事实上,保证无投掷拷贝构造函数并不是那么难。您应该只在异常中存储指向动态分配内存的共享指针:
class my_exception : virtual public std::exception {
public:
// public interface...
private:
shared_ptr<internal> representation;
};
假设异常是针对异常情况的,原子计数开销可以忽略不计。实际上这就是Boost.Exception所做的。
话虽如此,我也推荐jalf的答案。