【问题标题】:How to construct a <stdexcept> or <system_error> exception without throwing?如何构造 <stdexcept> 或 <system_error> 异常而不抛出?
【发布时间】:2016-03-19 20:26:29
【问题描述】:

&lt;stdexcept&gt; 中定义的异常(例如std::logic_errorstd::runtime_error 及其子类,例如std::system_error)具有期望字符串参数的构造函数,例如:

domain_error(const string& what_arg);
domain_error(const char* what_arg);

后置条件

strcmp(what(), what_arg.c_str()) == 0
strcmp(what(), what_arg) == 0

分别。不要求传递给构造函数的这些参数在这些异常的生命周期内保持有效,因此确保后置条件成立的唯一方法是复制并存储这些动态字符串。这需要内存,所以我假设它们的构造本身可能会抛出std::bad_alloc 或类似的东西,这通常是最出乎意料的。这会导致问题,因为我在野外看到的每个代码示例都鼓励人们编写类似

的代码
if (haveError)
    throw std::runtime_error("BOO!"); // May throw std::bad_alloc instead?!

而在其他地方预先构造异常似乎更安全,例如:

struct A {
    // During allocation of A one would often expect std::bad_alloc anyway:
    A() : m_someException("BOO!") {}
    void f() {
        /* Do stuff */
        if (haveError)
            throw m_someException;
            /* Note that according to §18.8.1.2 all standard library
               classes deriving from `std::exception` must have publicly
               accessible copy constructors and copy assignment operators
               that do not exit with an exception. In implementations such
               exception instances most likely share the common string
               with all their copies. */
    }
    std::runtime_error const m_someException;
};

这让我对抛出任何此类异常的库非常谨慎,例如,即使是 C++11 中 &lt;regex&gt; 中的 regex_error !!!

为什么这些异常没有 no-throw/noexcept 构造函数? C++ 核心指南对此有发言权吗?

PS:我个人会在异常祖先链中的这一点上留下what() 一个纯抽象方法。

EDIT 09.10.2017:这是一个证明 std::runtime_error 构造可以抛出 std::bad_alloc 的 PoC:

#include <cstddef>
#include <cstdlib>
#include <new>
#include <stdexcept>
#include <string>

bool throwOnAllocate = false;

void * operator new(std::size_t size) {
    if (!throwOnAllocate)
        if (void * const r = std::malloc(size))
            return r;
    throw std::bad_alloc();
}

void operator delete(void * ptr) { std::free(ptr); }

int main() {
    std::string const errorMessage("OH NOEZ! =(");
    throwOnAllocate = true;
    throw std::runtime_error(errorMessage);
}

【问题讨论】:

  • “而在其他地方预先构造异常似乎更安全”——我不明白。如果您的throw std::runtime_error("BOO!"); 由于没有足够的可用内存而引发不同的异常,则预先分配std::runtime_error仍然要求该内存可用。我看到的唯一变化是bad_alloc 会更早被抛出。
  • 如果不会抛出如此严重的异常,那就更可怕了。不要为小事操心,如果 bad_alloc 迫在眉睫,那么当你要扔的时候发生它并不重要,它总是会发生,你永远不会对此感到高兴。
  • 我猜如果你担心程序在 100% 错误安全条件下运行,你不应该使用 stdexcept,而是你自己的异常,它存储指向 c-strings 的指针,初始化为程序启动或类似的,您可以控制内存影响。
  • 我认为你不应该为 使用 runtime_error 而烦恼,毕竟正则表达式可能也会在正常工作期间分配内存。如果您想处理 OOM,则根本不应该使用标准正则表达式,否则您应该准备好处理这些错误。
  • @OP 即使他们有 noexcept ctors——它也不会帮助你。见this。我建议只是从 any throw 语句中期待 std::bad_alloc 。很容易理解为什么一旦你意识到异常和 C 风格的 EH 之间的主要区别——后者在函数签名中提到了错误对象类型,并且调用者在堆栈上分配了所需的内存。除了例外情况,您不能这样做——必须使用某种“动态”分配。

标签: c++ exception out-of-memory


【解决方案1】:

简短的回答,不可能构造任何绝对保证没有异常的对象。

您可以考虑在堆栈上分配,但您的线程堆栈可能会用完并导致系统错误/异常。你的 CPU 可能会在你抛出时得到一个外部中断,那个系统无法处理,一切都被烧毁了。正如其他人所建议的那样,不要为小事烦恼。内存不足是大多数用户程序无法恢复的,所以不用担心,优雅地失败。不要试图处理每一个糟糕的情况,只处理那些你可以轻松恢复的情况。

附带说明一下,对于内存耗尽的情况,许多高图形游戏在游戏初始化期间预先完成所有堆分配,并尽量避免在游戏开始后出现问题游戏中间内存不足/分配缓慢(紧张的游戏和糟糕的用户体验)。同样,您也可以巧妙地设计程序,以减少遇到不良情况的机会。

【讨论】:

  • 感谢您提供有关预分配内存的提示。 +1
  • 有可能。堆栈溢出是一种非常特殊的情况,因为除非您使用递归算法,否则您的程序将需要固定数量的堆栈空间,而不管其输入的大小。对于动态内存分配,这是不同的,因为通常更多的输入需要更多的内存来处理它。如果输入足够大,大多数程序都会遇到 OOM。
【解决方案2】:

你不能在没有得到std::bad_alloc的情况下构造std::logic_errorstd::runtime_error,但大多数时候这并不重要。如果操作失败,无论如何都必须提供单独的代码路径来处理这个问题,同样的代码路径也可以用于处理std::bad_alloc。在重要的极少数情况下,您应该直接从std::exception 派生,并使what() 成员函数返回一个固定字符串。大多数情况下,除非另有明确说明,否则“函数 X 抛出 Y”应理解为“函数 X 抛出 Y 和 std::bad_alloc”。这也是为什么 C++11 放弃了指定的 throw() 以支持 noexcept()

我认为预先分配异常没有帮助,因为如果您遇到std::bad_alloc,此时丢失一些错误信息可能是一个不错的选择,因为这种情况很少见且不值得麻烦。作为正常工作的一部分,调用代码可以假设此类函数由于内存分配失败而失败。

现在,如果您担心由于在处理另一个异常期间抛出异常而丢失错误信息,您可以尝试查看异常链接(或异常嵌套)。这方面的例子可以在其他语言中找到:

C++11 提供了使用std::throw_with_nested()std::rethrow_if_nested() 嵌套异常的标准方法。不幸的是,如果你在 catch() 块之外抛出异常,稍后使用 std::rethrow_if_nested() 将终止你的程序,所以这个设施 IMO 有点坏了。如果您真的关心这些问题,您可以实现自己的变体,它可以进行显式和隐式链接(您需要std::current_exception() 来执行此操作)。您将无法强制外部库使用您的设施,但至少您的代码在这方面可以非常先进。

【讨论】:

    【解决方案3】:
    if (haveError)
        throw std::runtime_error("BOO!"); // May throw std::bad_alloc instead?!
    

    当您到达throw 时,您应该已经完成​​了所有清理工作,因此在大多数情况下,抛出std::bad_alloc 而不是std::runtime_error 并不会产生太大的影响。

    我能做到的唯一例外情况是当异常用于控制程序的流程时——我经常使用以下代码来执行此操作:

    try { 
      auto face = detectFace(); // may throw a custom no_face_exception or a many_faces_exception
      // do work with face
    } 
    catch (no_face_exception& e) {
      std::cout << "no face detected\n";
    }
    catch (many_faces_exception& e) {
      std::cout << "too many faces detected\n";
    }
    

    在这种特殊情况下,分配内存失败会导致detectFace 抛出std::bad_alloc,这将导致灾难性的崩溃。正如您所建议的,在抛出之前首先分配异常不会改变任何东西 - 程序仍然会因分配失败而崩溃 std::bad_alloc。解决此问题的唯一方法是简单地捕获std::bad_alloc

    catch (std::bad_alloc& e) {
      std::cout << "bad alloc reported\n";
    }
    

    【讨论】:

      【解决方案4】:

      仅当您尝试使用内置异常类型时,您的问题才是正确的。 尽管此线程中的其他一些答案说, 抛出和失败之间存在差异,并且 抛出不同类型的异常之间存在差异。

      我怎么能这么说?因为这就是异常机制的构建方式。

      在堆栈上分配失败不会提供简单的恢复,而在堆上分配失败只会抛出。 有所不同,因为 有一种方法可以从堆故障中恢复。 同样,可以catch 不同的异常,因为人们可能会以不同的方式处理它们。 这样的机制,因为恢复可能非常不同。

      此外,虽然bad_alloc 在大多数平台和应用程序中可能不是很常见,但它是人们应该注意的事情,尤其是在内存较小和/或应用程序需要的情况下很多。是的,从bad_alloc 恢复有时可能与从其他问题(如数据库错误或连接问题)中恢复非常不同。但是,如果您在尝试只为非常短的消息分配足够的空间时收到bad_alloc,则确实存在严重问题,但仍然可能有恢复选项(例如:如果您持有大量数据仅用于缓存目的,您可以释放它们)。

      尽管如此,解决方案非常简单 - std::exceptionwhat() 函数是虚函数并非巧合,它返回最简单的类型 - const char *。您可以直接从std::exception 继承您想要的任何异常,并以不在堆上分配的方式实现它。例如,将消息作为类的静态成员(可能带有一些值的占位符)或仅将其作为字符串文字提供。这样您就可以确保在投掷时不会进行新的分配。

      【讨论】:

        猜你喜欢
        • 2018-05-20
        • 1970-01-01
        • 1970-01-01
        • 2019-07-11
        • 2011-11-04
        • 2012-01-24
        • 1970-01-01
        • 2014-11-03
        • 1970-01-01
        相关资源
        最近更新 更多