在C++中从静态对象的构造函数中抛出/捕获异常

4
我有一个需求,在C'tor中需要读取输入文件,但有时该文件不存在。 这个对象通常是静态持有的,因此在加载dll时会调用它的C'tor。 如果文件不存在,我无法捕获抛出的异常,因为这太早了,我的可执行文件会以不好看的方式崩溃。 我知道从C'tor中抛出异常是不好的做法,但如果文件不存在,我必须这样做。 是否有一种方法可以在加载dll时捕获异常?如果没有,是否有更好的解决方案呢?
谢谢, Gal
7个回答

8

我假设这个静态对象具有文件作用域(它在任何函数/类定义之外)。您可以考虑将其移动到访问器函数中,并仅通过该函数进行访问,就像这样:

class Object;
Object& getObject()
{
    static Object object;
    return object;
}

当第一次调用getObject()方法时,Object的静态实例将被初始化。如果Object的构造函数抛出异常,您可以轻松捕获异常。只需记住将每个getObject()调用包装在try/catch块中(否则异常会向上冒泡),这可能有点不方便,但另一方面,如果您知道程序逻辑流程中的逻辑“第一个”调用是哪个,您可以决定仅包装该调用。


应该是这样写:static Object object; 吧? - anon
是的,我也这样认为。我会更改它,因为我会给出相同的答案。Vlado,希望你不介意?如果有问题,请撤销更改 :) - Johannes Schaub - litb
哦,当然,是的,绝对没问题 ;-)。我一定是瞎了。 - Vlado Klimovský

3

在构造函数中抛出异常并不一定是一种不好的实践。事实上,RAII通常需要您这样做,因为对象具有必须满足的内部不变量,如果构造函数无法初始化对象并使其处于有效状态,则这是唯一的方法。

另一方面,在析构函数中抛出异常是一种不好的实践(也是一种危险的实践)。但是,在构造函数中抛出异常应该是可以接受的。


从构造函数中抛出异常是指示对象无法构造的唯一方法。构造函数没有提供任何其他语义来实现它。另一方面,从析构函数中抛出异常将终止您的程序(这可能在规格中;我没有带着它们)。 - Euro Micelli

2

如果您可以使用c++11,则可以使用lambda和unique_ptr<>解决此问题:

// In some_file.hpp
#pragma once
#include <memory>
#include <stdexcept>

class CtorThrows {
public:
  CtorThrows (int value) {
    if (value < 10) {
      throw std::runtime_error("oops!");
    }
  }
};

class ClassWithStatic {
public:
private:
  static std::unique_ptr<CtorThrows> bad_member_; // <-- static member
};

然后

// In some_file.cpp
#include "some_file.hpp"

// Create a lambda function to initialize the static member variable.
std::unique_ptr<CtorThrows> ClassWithStatic::bad_member_ = []() {
  try {
    int value = 5;  // in this case, it is a bad value

    // This only returns successfully if bad_value DOESN'T cause
    // the ctor to throw and exception.
    return std::make_unique<CtorThrows>(value);
  } catch (std::runtime_error &e) {
    std::cerr << "OOPs!  Here's a nice error message" << std::endl;
    exit(1);
  }
  return std::unique_ptr<CtorThrows>(nullptr);
}();

使用unique_ptr可以让你即使在类具有删除或私有复制构造函数和复制赋值运算符的情况下也能实现这一点。

1
重新设计对象,使其在稍后打开文件 - 例如当第一次请求文件中的数据时。
或者用静态指针替换静态对象,并在需要时调用new。最好使用智能指针,如auto_ptr。

是的,但是在任何getter中我都必须检查它是否已经初始化,这样看起来很不美观。 - Gal Goldman
你可以创建一个 openIfNeeded() 方法来检查和打开,然后在每个 getter 方法中调用它。是的,这并不出色和美观,但也不太丑陋,并且解决了你的问题。 - sharptooth

1
如何将读取输入文件与构造函数分离?您可以拥有一个单独的Init()方法,在构造对象后但在对象实际可用之前必须调用该方法。

正如我之前所评论的,我不想在对象准备好后再调用init,因为这样每次获取器都必须检查它是否已经初始化,而且很丑陋。 - Gal Goldman
在这种情况下(除非我完全误解了你的问题),您应该使用单例模式,并让单例获取方法正确地初始化对象。 - Adrian Grigore

1

你说得对,你无法捕获在静态对象初始化期间发生的异常。

由于你正在编写一个DLL:每个DLL都可以有一个入口点,在这个入口点内部,异常处理起作用。(这与主程序中的main函数相同)。我建议删除你的类的静态实例,用指针替换它们,并在dllmain中初始化这些指针。

这将彻底解决你的问题。

顺便说一下 - DLL入口点会在加载、卸载和其他事件(如进程附加/分离等)时被调用。请确保使用正确的位置来初始化你的类。


0
一种方法是“设计”这个过程,使调用代码(即dll之外的代码)负责确保dll的所有依赖关系已经准备就绪。在调用代码中编写一个函数,确保该dll的依赖项(在本例中是文件)已准备就绪并可被加载,然后再加载该库。如果没有准备就绪,程序可以优雅地退出。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接