问题 我的C ++异常类真的需要多精益?


有很多地方可以找到设计异常类的指南。几乎在我看的每个地方,都有异常对象永远不应该做的事情列表,这会影响这些类的设计。

例如, 提升人们的推荐 该类包含否 std::string 成员,因为他们的构造函数可以抛出,这将导致运行时立即终止程序。

现在,在我看来,这是理论上的。如果 std::string的构造函数抛出,它是一个bug(我传入一个空指针)或一个内存不足的情况(如果我错在这里纠正我)。由于我在桌面上,我只是假装我有无限的内存,而且 内存耗尽对我的应用程序来说是致命的,无论如何

考虑到这一点,我为什么不应该嵌入 std::string 我的异常类中的对象?事实上,为什么我的异常类不能全功能,并且还要处理日志记录,堆栈跟踪等。我知道一个责任原则,在我看来,这是一个公平的权衡让异常类完成所有这些。当然,如果我的解析器需要报告语法错误,那么功能齐全的异常将比围绕静态分配的字符数组构建的异常更有帮助。

那么:精益C ++异常类 - 它在现实世界中的交易有多大?有什么权衡取舍?是否有关于该主题的良好讨论?


10446
2018-01-19 09:36


起源



答案:


您可以使用Boost.Exception库来帮助定义异常层次结构。 Boost.Exception库支持:

将任意数据传输到   抓住网站,这是非常棘手的   由于无投掷要求   (15.5.1)异常类型。

框架的局限性将为您提供合理定义的设计参数。

Boost.Exception
也可以看看: Boost.System


3
2018-01-19 11:09





作为一般情况,异常类应该是简单的,自给自足的结构,并且永远不会分配内存(比如 std::string 一样)。第一个原因是分配或其他复杂操作可能会失败或产生副作用。另一个原因是异常对象按值传递,因此是堆栈分配的,因此它们必须尽可能轻量化。更高级别的功能应该由客户端代码处理,而不是异常类本身(除非出于调试目的)。


2
2018-01-19 09:42



嗯,这正是我的问题:如果我假设我拥有世界上所有的内存,并且在异常处理中性能无关紧要,为什么我需要采取这些预防措施?为什么我的解析失败的异常不允许分配,如果它使错误处理更容易? - Carl Seleborg
即使有无限的内存即时CPU操作,异常仍然必须避免副作用,因为根据定义,它们会修改应用程序的状态,并可能反过来自己抛出异常。你的班级越复杂,副作用就越大。 F.ex.避免记录您的异常。 - fbonnet
为什么你认为你拥有世界上所有的记忆?当您的应用因内存泄漏而崩溃时会发生什么?如果你故意抛弃处理异常的所有希望,你将如何找到泄漏? - jalf
异常对象不一定是堆栈分配的;它们以未指定的方式分配。您永远不应该按值捕获异常对象,因为这可能会导致切片,始终通过引用捕获。除了必须不抛出的复制构造函数之外,异常类型构造函数很复杂,包括分配内存并在它们无法初始化异常对象时抛出。 - Emil


答案:


您可以使用Boost.Exception库来帮助定义异常层次结构。 Boost.Exception库支持:

将任意数据传输到   抓住网站,这是非常棘手的   由于无投掷要求   (15.5.1)异常类型。

框架的局限性将为您提供合理定义的设计参数。

Boost.Exception
也可以看看: Boost.System


3
2018-01-19 11:09





作为一般情况,异常类应该是简单的,自给自足的结构,并且永远不会分配内存(比如 std::string 一样)。第一个原因是分配或其他复杂操作可能会失败或产生副作用。另一个原因是异常对象按值传递,因此是堆栈分配的,因此它们必须尽可能轻量化。更高级别的功能应该由客户端代码处理,而不是异常类本身(除非出于调试目的)。


2
2018-01-19 09:42



嗯,这正是我的问题:如果我假设我拥有世界上所有的内存,并且在异常处理中性能无关紧要,为什么我需要采取这些预防措施?为什么我的解析失败的异常不允许分配,如果它使错误处理更容易? - Carl Seleborg
即使有无限的内存即时CPU操作,异常仍然必须避免副作用,因为根据定义,它们会修改应用程序的状态,并可能反过来自己抛出异常。你的班级越复杂,副作用就越大。 F.ex.避免记录您的异常。 - fbonnet
为什么你认为你拥有世界上所有的记忆?当您的应用因内存泄漏而崩溃时会发生什么?如果你故意抛弃处理异常的所有希望,你将如何找到泄漏? - jalf
异常对象不一定是堆栈分配的;它们以未指定的方式分配。您永远不应该按值捕获异常对象,因为这可能会导致切片,始终通过引用捕获。除了必须不抛出的复制构造函数之外,异常类型构造函数很复杂,包括分配内存并在它们无法初始化异常对象时抛出。 - Emil


在允许代码处理异常的任何考虑之前,异常的头号工作是能够向用户和/或dev确切地报告出错的地方。一个异常类,它不能报告OOM,但只是崩溃程序而不提供任何线索,为什么它崩溃是不值得的。 OOM现在变得非常普遍,32位虚拟内存耗尽了气体。

向异常类添加大量辅助方法的麻烦在于它会强制您进入一个您不一定想要或不需要的类层次结构。现在需要从std :: exception派生,因此您可以使用std :: bad_alloc执行某些操作。当您使用具有不是从std :: exception派生的异常类的库时,您将遇到麻烦。


2
2018-01-19 16:38



通常,您不能依赖尝试抛出异常对象而不会传播另一个异常对象。除了复制构造函数之外,异常类型构造函数可能会抛出。 - Emil


看看他们都在内部使用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(或其他标准异常之一)派生我的异常。


2
2018-01-20 08:25



在异常类型中使用std :: string作为成员违反了ANSI C ++标准的异常类型的无抛出要求(15.5.1)。此外,异常类型层次结构应使用虚拟继承,请参阅 boost.org/doc/libs/1_45_0/libs/exception/doc/...。 - Emil
@Emil:这是STL的剪切和粘贴。此外,它不会破坏要求,因为它必须抛出(我确信STL的实现者在实现此代码时考虑到了它)。从多个类继承异常是非常奇怪的(非传统的)(我以前从未见过它)。将MH的额外复杂性添加到异常似乎是不合逻辑的,因此我不同意该注释,并仍然建议继承自std :: runtime_error。 - Martin York


由于我在桌面上,我只是假装我有无限的内存,无论如何,内存耗尽对我的应用程序来说是致命的。

因此,当您的应用程序致命失败时,您不希望它干净地终止吗?让析构函数运行,文件缓冲区或日志被刷新,甚至可能向用户显示错误消息(甚至更好,错误报告屏幕)?

考虑到这一点,为什么我不应该在我的异常类中嵌入std :: string对象?事实上,为什么我的异常类不能全功能,并且还要处理日志记录,堆栈跟踪等。我知道一个责任原则,在我看来,这是一个公平的权衡让异常类完成所有这些。

为什么这是一个公平的权衡?为什么这是一个权衡取舍 所有?权衡意味着你做出来 一些 对单一责任原则的让步,但据我所知,你不这样做。你只是说“我的例外应该做的一切”。这不是一个权衡。

与SRP一样,答案应该是显而易见的:通过使异常类完成所有工作,您获得了什么?为什么记录器不能成为一个单独的类?为什么必须由异常执行?不应该由例外处理 处理器?您可能还希望进行本地化,并提供不同语言的语法错误消息。所以你的异常类在构造时应该出去读取外部资源文件,寻找正确的本地化字符串?这当然意味着另一个潜在的错误源(如果找不到字符串),会增加异常的复杂性,并且需要异常才能知道其他无关信息(用户使用哪种语言和区域设置)。格式化的错误消息可能取决于它的显示方式。也许它在记录时,在消息框中显示或打印到stdout时应格式不同。处理异常类的问题比较多。还有更多可能出错的事情,更多可能发生错误的代码。

您的异常尝试越多,出错的可能性就越多。如果它尝试记录,那么如果磁盘空间不足会发生什么?也许你也假设无限的磁盘空间,只是忽略它,如果它发生,你将丢弃所有的错误信息? 如果您没有日志文件的写权限怎么办?

根据经验,我不得不说,有些事情比较烦人 没有获得有关刚刚发生的错误的任何信息,因为发生了错误。如果您的错误处理无法处理错误发生,则不是真正的错误处理。如果异常类无法处理被创建和抛出而不会导致更多异常, 有什么意义呢

通常,SRP的原因是您添加到类中的复杂性越高,确保正确性就越困难,并且 理解 代码。这仍然适用于异常类,但您还会得到第二个问题:您添加到异常类的复杂性越高,发生错误的机会就越多。通常,您不希望发生错误 同时抛出异常。毕竟,你已经处理了另一个错误。

但是,“异常类不应包含的规则” std::string 与“不允许异常类分配内存”不完全相同。 std::exception 做后者。它毕竟存储了一个C风格的字符串。 Boost只是说不存储可能引发异常的对象。因此,如果您分配内存,您只需要能够处理分配失败的情况。

当然,如果我的解析器需要报告语法错误,那么功能齐全的异常将比围绕静态分配的字符数组构建的异常更有帮助。

说刚才说他不介意应用程序的人只是在发生错误时没有向用户反馈而终止。 ;)

是的,您的异常应包含生成友好,可读的错误消息所需的所有数据。在解析器的情况下,我会说必须是这样的:

  • 输入文件名(或允许我们在需要时获取文件名的句柄或指针)
  • 发生错误的行号
  • 也许这个位置就行了
  • 发生的语法错误类型。

根据这些信息,您可以 处理错误时 为用户生成一个友好,健壮的错误消息。如果您愿意,甚至可以将其本地化。你可以本地化它 当你处理异常时

通常,异常类供程序员使用。它们不应包含或构建针对的文本 用户。创建正确可能很复杂,应该在处理错误时完成。


2
2017-09-06 13:31



Jalf,你误解了我写的内容:我说过,如果内存耗尽,我可以/不愿意这么做(当然不会弹出一个窗口)。在任何其他情况下,我希望我的异常包含尽可能多的信息,这意味着分配内存(这反过来意味着可能抛出异常)。权衡是在一个更难维护的类和数千个呼叫站点之间,我需要一行代码来收集大量有关错误的数据。 - Carl Seleborg
不,我想你误解了我的观点。是的,该异常应该包含足够的数据来描述发生的问题。但它不必持有描述它的用户友好消息。它只需要保存构造这样一条消息所必需的数据,那就可以了 经常 (不 总是)没有动态分配。在耗尽内存的情况下,您可以做更多事情。您可以记录异常,以便开发人员稍后处理它。但是,如果简单地构造异常失败,则异常将不会被记录,因为它试图分配内存。 - jalf


C ++标准要求异常具有无抛出复制结构。如果您有一个std :: string成员,则没有无抛出的复制构造函数。如果系统无法复制您的例外,它将终止您的程序。

在设计异常类型层次结构时使用虚拟继承也是一个好主意,如中所述 http://www.boost.org/doc/libs/release/libs/exception/doc/using_virtual_inheritance_in_exception_types.html

但是,不要求异常对象很简单或不分配内存。实际上,异常对象本身通常在堆上分配,因此系统可能会耗尽内存以尝试抛出异常。


1
2018-01-14 07:17





我认为拒绝在异常类中使用std :: string是不必要的纯粹主义。是的,它可以抛出。所以呢?如果你执行std :: string抛出 出于内存不足的原因 只是因为你正在构造一条消息“无法解析文件Foo”,那么实现有问题,而不是你的代码。

至于内存不足,即使构造一个不带字符串参数的异常,也会出现这个问题。添加20个字节的有用错误消息不太可能造成或破坏。在桌面应用程序中,当您尝试错误地分配20 GB的内存时,会发生大多数OOM错误,这不是因为您一直在以99.9999%的容量运行,并且有些东西让您超过顶部。


0
2017-09-06 13:08



如果在分配字符串时内存不足,那么“实现有问题”?如何实施是错误的 你的 内存泄漏? - jalf
我澄清了我的回答,以便其他读者不会像你那样误解它。 - quant_dev


......无论如何,内存耗尽对我的应用程序来说是致命的。

不幸的是,这正是大多数人所说的,因为他们不想处理其他情况下出现的复杂性。另一方面,如果您遵循标准设置的要求,您将获得更强大的软件,即使在低内存条件下也可以恢复。

将火星车放在一边,想想像编写文本处理器这样简单的情况。您想要实现复制/粘贴功能。用户选择一些文本并按Ctrl + C.你喜欢什么结果:崩溃或消息“内存不足”? (如果没有足够的内存来显示消息框,则没有任何反应。)第二种情况无疑更加用户友好,她可以关闭其他应用程序并继续处理她的文档。

事实上,保证无投掷拷贝构造函数并不是那么难。您应该只在异常中存储指向动态分配的内存的共享指针:

class my_exception : virtual public std::exception {
public:
    // public interface...
private:
    shared_ptr<internal> representation;
};

假设例外情况适用于特殊情况,原子计数开销可以忽略不计。实际上这就是Boost.Exception的作用。

话虽如此,我也推荐jalf的回答。


0
2017-10-19 09:41