【问题标题】:C++, an exception in the constructor of a static object bypasses destructors of prior static objectsC ++,静态对象的构造函数中的异常绕过先前静态对象的析构函数
【发布时间】:2016-08-06 15:01:35
【问题描述】:

我正在研究如何添加 C++ 异常处理来处理现有实时应用程序中的运行时错误。我从构建对象的失败开始,这些对象封装了硬件系统组件的驱动程序,例如位于 Raspberry Pi 平台的 SPI 总线上的电源监控微控制器。

遵循 RAII 原则,我必须在它们的构造函数中完全初始化这些对象,如果系统资源不可用,例如如果 SPI 驱动程序没有加载,这会导致失败的可能性。由于构造函数没有返回值,因此我必须使用异常来处理此类失败。

这些硬件驱动程序对象是静态的(文件范围)。为什么?这样它们的构造函数就会被自动调用,更重要的是,它们的析构函数会在程序退出时自动调用。此外,我需要从主程序文件中的任何位置获取对象,因为它们代表全局硬件资源。

我并不关心异常是如何处理的(我可以在抛出之前发出有用的错误信息),但我关心程序是否正确终止。

发生的情况是,如果静态分配对象的构造函数失败并抛出异常,那么在失败对象之前的词法上其他静态对象的析构函数不会被调用。我已经在一个最小的测试台上对此进行了测试。 Apple、Pear 和 Orange 类只有构造函数和析构函数,它们向标准输出宣布自己,除了 Orange 的构造函数随后抛出异常。在主文件中,我按顺序定义了 Apple、Pear 和 Orange 的一个静态实例。构造函数在程序执行时被调用,Orange 抛出异常,程序结束而不调用 Apple 和 Pear 的析构函数。

我在这里错过了什么?

在类似问题的答案中,例如556655,人们建议: - 不在构造函数中抛出异常。嗯? - 有一个单独的初始化方法,在构造后称为“手动”,以执行任何可能失败的操作。那么RAII呢? (顺便说一句,这就是我现在拥有的东西,没有例外)。 - 将静态对象更改为指针并使用 new 运算符“手动”调用构造函数。然后我必须在失败的情况下调用正确的析构函数,我希望使用异常可以避免这种情况。 - 将每个静态对象包装在另一个对象中,该对象具有访问器函数以获取对其的引用。显然,直到第一次在外部对象上调用访问器时才会调用内部对象的构造函数,这将允许我捕获异常,并且可能会导致整洁的退出。这似乎是一个可怕的组合。

回想一下,我不需要捕获异常,程序可以随意终止。这使我的问题与我发现的其他问题不同。我的问题是,为什么不为成功构造的静态对象调用析构函数?

格雷厄姆。

【问题讨论】:

  • 我们又来了。全局变量,静态全局变量(接下来:单例 - arrrgh;痛苦,很痛苦)。如果你想避免很多痛苦:不要使用全局变量,甚至文件范围的静态变量。只是不要
  • 我理解您对使用文件范围变量的反感。我尽我所能避免它们,但在这个程序中,我无法弄清楚如何以任何合理的方式做到这一点。我有封装只读命名管道、只写命名管道和三个硬件子系统驱动程序的文件范围对象。命令通过一个管道到达,我操纵硬件执行命令并通过另一个管道生成响应。我必须跟踪硬件的状态,以使在某些操作完成之前无法执行的命令失败。
  • [继续] ...如果我不将驱动程序对象设为全局(或将它们打包在全局包装器对象中,我认为这不能解决问题)我必须传递它们通过命令解析和执行函数的层次结构作为参数(除非我遗漏了什么并且已经有几十年了)。
  • @GrahamDavies:您可以使用全局指针而不是全局对象,并在 main 中初始化它们。这将完全避免整个静态初始化混乱。你仍然会有丑陋的全局变量,但你可以稍后再决定:)

标签: c++ exception constructor static


【解决方案1】:

您错过了未处理的异常。当程序由于未处理的异常而终止时,不能保证调用本地和静态对象的析构函数(我记得它是定义的实现)。一种解决方案是将这些对象放在一个通用的包装器对象中,在maintry 块中创建它,然后让指向它的指针可用于当前全局变量可能存在的所有任意访问。

草图:

class Drivers
{
friend auto main() -> int;
    // ...
};

namespace impl {
    Drivers* p_drivers;
}  // namespace impl

auto drivers() -> Drivers& { return *impl::p_drivers; }

auto main()
    -> int
{
    try
    {
        Drivers drivers;
        impl::p_drivers = &drivers;
        // ...
        return EXIT_SUCCESS;
    }
    catch( exception const& x )
    {
        log_failure( x.what() );
    }
    return EXIT_FAILURE;
}

【讨论】:

  • 我在这个答案中挖出了标准报价:stackoverflow.com/questions/32323406/…
  • "...未处理异常。"这可能是我所缺少的。我一直误认为 stdout / stderr 上出现的内容是处理异常,而实际上它是“实现定义的”行为。我特别被我在抛出时传递给异常构造函数的字符串的外观所误导。我认为自从我的字符串出现以来,启动代码中就有一个异常处理程序,它调用静态对象的构造函数。现在我看到了我的错误。
  • 因此,RAII(以及因此构造函数中的异常)似乎与文件范围对象不兼容。解决方案似乎都是在块范围内创建对象,使用 new 运算符或在进入块时自动创建,以及在文件范围内初始化一个或多个指针以提供我认为我需要的通用访问。将文件范围对象打包为“驱动程序”似乎是一个好主意,尽管在我看来它并没有避免对文件范围对象的基本反对(这究竟是什么?)。
  • @GrahamDavies:正确的总结。许多代码可以访问的可变文件范围对象的问题在于它们提供了不可见且难以找到的通信和控制线。人们永远无法确定代码的哪些部分会影响其他部分,什么时候保证初始化,这些值有多可靠(它们来自哪里?)。使用这些开放的通信设施,n 段代码具有 n×(n-1)/2 条通信和控制线,即需要考虑 O(n²) 影响。这就是复杂性。这会导致错误和许多不必要的冗余工作。
  • 对文件范围对象的基本反对意见:是的,这就是我理解的反对意见;您可以从很多地方访问它们。因此,将它们作为参数传递同样糟糕。如果我无法重新构建我要解决的问题,以便我不需要从各处访问我的硬件驱动程序,那么将它们作为文件范围对象与其他任何东西一样好,我只需要编写驱动程序以保护自己免受误用,并努力使代码透明且易于验证。 (我没有找到任何其他可以正确解决此问题的帖子。)
猜你喜欢
  • 2012-12-26
  • 2023-04-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-11
  • 1970-01-01
  • 1970-01-01
  • 2012-11-27
  • 1970-01-01
相关资源
最近更新 更多