代码之家  ›  专栏  ›  技术社区  ›  Carl Seleborg

我的C++异常类到底需要多精简?[闭门]

  •  11
  • Carl Seleborg  · 技术社区  · 16 年前

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

    例如 Boost people recommend 该类不包含 std::string 成员,因为他们的构造函数可以抛出,这将导致运行时立即终止程序。

    现在,在我看来,这是相当理论化的。如果 std::string 的构造函数抛出,它要么是一个bug(我传入了一个空指针),要么是内存不足(如果我在这里出错,请纠正我)。因为我在桌面上,我就假装我有无限的内存,然后 内存不足对我的应用程序来说是致命的 .

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

    所以:精益C++异常类——在现实世界中有多重要?取舍是什么?关于这个话题有好的讨论吗?

    8 回复  |  直到 16 年前
        1
  •  3
  •   Functastic    16 年前

    你可以用助推器。异常库来帮助定义异常层次结构。 刺激。异常库支持:

    将任意数据传输到 捕获站点,否则会很棘手 由于无投掷要求 (15.5.1)适用于例外类型。

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

    Boost.Exception
    另见: Boost.System

        2
  •  2
  •   fbonnet    16 年前

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

        3
  •  2
  •   Hans Passant    16 年前

    在考虑允许代码处理异常之前,异常的首要任务是能够准确地向用户和/或开发人员报告出了什么问题。一个异常类不能报告OOM,但只是在不提供任何关于程序崩溃原因的线索的情况下崩溃了程序,这是不值得的。OOM现在变得非常普遍,32位虚拟内存已经耗尽。

    向异常类中添加大量帮助器方法的问题在于,它会迫使您进入一个不一定想要或需要的类层次结构。现在需要从std::exception派生,这样就可以用std::bad_alloc做一些事情。当您使用一个库时,如果它包含的异常类不是从std::exception派生的,那么您就会遇到麻烦。

        4
  •  2
  •   Loki Astari    16 年前

    看看std异常,它们都在内部使用std::string。
    (或者我应该说我的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();
    };
    

    我通常从运行时错误(或其他标准异常之一)派生出我的异常。

        5
  •  2
  •   Stack Overflow is garbage    15 年前

    因为我在桌面上,我就假装我有无限的内存,不管怎样,内存耗尽对我的应用程序来说都是致命的。

    所以,当你的应用程序致命失败时,你不希望它完全终止吗?让析构函数运行,刷新文件缓冲区或日志,甚至可能向用户显示一条错误消息(或者更好,显示一个错误报告屏幕)?

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

    为什么这是一个公平的权衡?为什么这是一种权衡 全部的 ? 权衡意味着你要 一些 对单一责任原则做出让步,但据我所知,你不会这么做。你只是简单地说“我的例外应该做一切”。这几乎不是一种权衡。

    和SRP一样,答案应该是显而易见的:让exception类做所有事情,你会得到什么?为什么记录器不能是一个单独的类?为什么必须由异常执行?它不应该由异常处理吗 处理者 ? 您可能还希望进行本地化,并提供不同语言的语法错误消息。所以,在构造异常类时,应该走出去读取外部资源文件,寻找正确的本地化字符串?这当然意味着另一个潜在的错误源(如果找不到字符串),会增加异常的复杂性,并要求异常知道其他不相关的信息(用户使用的语言和语言环境设置)。格式化的错误消息可能取决于它的显示方式。也许在记录、显示在消息框中或打印到标准输出时,它的格式应该不同。exception类要处理的更多问题。更多可能出错的事情,更多可能出错的代码。

    你的异常尝试的越多,出错的事情就越多。如果它试图登录,那么如果磁盘空间不足会发生什么?也许你也会假设无限的磁盘空间,只是忽略了如果发生这种情况,你会扔掉所有的错误信息? 如果您没有日志文件的写入权限怎么办?

    根据经验,我不得不说,没有什么事情比这更烦人了 由于发生错误,无法获取有关刚刚发生的错误的任何信息 .如果您的错误处理无法处理发生的错误,那就不是真正的错误处理。如果异常类无法在不引发更多异常的情况下处理创建和抛出的异常, 重点是什么 ?

    通常,使用SRP的原因是,向类添加的复杂性越高,就越难确保正确性,也就越难实现 密码。这仍然适用于异常类,但您还需要考虑第二个问题:添加到异常类的复杂性越高,发生错误的机会就越多。一般来说,你不希望出现错误 在抛出异常时 。毕竟,您已经在处理另一个错误。

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

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

    表示他不介意应用程序终止的人说,如果出现错误,他不会向用户提供反馈

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

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

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

    通常,异常类供程序员使用。它们不应包含或构建针对 使用者 。正确创建可能很复杂,在处理错误时应该这样做。

        6
  •  1
  •   Emil    15 年前

    C++标准要求异常没有抛出拷贝构造函数。如果你有一个std::string成员,你就没有一个no-throw-copy构造函数。如果系统无法复制你的异常,它将终止你的程序。

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

    然而,不要求异常对象简单或不分配内存。事实上,异常对象本身通常是在堆上分配的,因此系统可能会在试图抛出异常时耗尽内存。

        7
  •  0
  •   quant_dev    15 年前

    我认为拒绝在异常类中使用std::string是不必要的纯粹主义。是的,它能扔。那又怎么样?如果std::string的实现抛出 因为内存不足以外的原因 仅仅因为您正在构造一条消息“无法解析文件Foo”,那么实现就有问题,而不是代码。

    至于内存不足,即使构造一个不带字符串参数的异常,也会出现这个问题。添加20字节有用的错误消息不太可能成功或失败。在桌面应用程序中,大多数OOM错误发生在你试图错误地分配20GB内存时,这并不是因为你一直在以99.9999%的容量愉快地运行,或者有什么事情让你大吃一惊。

        8
  •  0
  •   Yakov Galka    13 年前

    ... 内存不足对我的应用程序来说是致命的。

    不幸的是,这正是大多数人所说的,因为他们不想处理由此产生的复杂性。另一方面,如果您确实遵循了标准设置的要求,您将获得更健壮的软件,即使在内存不足的情况下也可以恢复。

    撇开火星漫游者不谈,想想这样一个简单的例子,比如编写一个文本处理器。您希望实现复制/粘贴功能。用户选择一些文本并点击Ctrl+C。您更喜欢哪种结果:崩溃或消息“内存不足”?(如果没有足够的内存来显示消息框,则什么也不会发生。)第二种情况无疑更加用户友好,她可以关闭其他应用程序,继续处理她的文档。

    事实上,保证不抛出复制构造函数并不难。您应该只在异常中存储一个指向动态分配内存的共享指针:

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

    假设异常是针对异常情况的,那么原子计数开销可以忽略不计。事实上,这就是提振。例外。

    话虽如此,我还是推荐jalf的答案。