3

在 C++ 程序中,我需要一些辅助常量对象,这些对象将被实例化一次,最好是在程序启动时。这些对象将主要在同一个翻译单元中使用,因此最简单的方法是将它们设为静态:

static const Helper h(params);

但是还有这个静态初始化顺序问题,所以如果Helper引用其他一些静态(通过params),这可能会导致UB。

另一点是我最终可能需要在几个单位之间共享这个对象。如果我只是把它static放在一个 .h 文件中,那将导致多个对象。我可以通过打扰externetc 来避免这种情况,但这最终会引发相同的初始化顺序问题(并不是说它看起来非常 C-ish)。

我考虑过单例,但由于样板代码和不方便的语法(例如MySingleton::GetInstance().MyVar),这将是矫枉过正 - 这些对象是助手,所以它们应该简化事情,而不是使它们复杂化......

相同的 C++ FAQ提到了这个选项:

 Fred& x()
 {
   static Fred* ans = new Fred();
   return *ans;
 } 

这真的被使用并被认为是一件好事吗?我应该这样做,还是您会建议其他替代方案?谢谢。

编辑:我应该澄清为什么我实际上需要那个助手:它们非常像普通常量,并且可以预先计算,但在运行时这样做更方便。我更愿意在 main 之前实例化它们,因为它会自动解决多线程问题(在 C++03 中本地静态不受保护)。此外,正如我所说,它们通常仅限于翻译单元,因此导出它们并在 main() 中初始化是没有意义的。您可以将它们视为常量,但仅在运行时才知道。

4

4 回答 4

5

全局状态有几种可能性(无论是否可变)。

如果您担心会遇到初始化问题,那么您应该使用该local static方法来创建您的实例。

请注意,您提供的笨重单例设计不是强制性设计:

class Singleton
{
public:
  static void DoSomething(int i)
  {
    Singleton& s = Instance();
    // do something with i
  }


private:
  Singleton() {}
  ~Singleton() {}

  static Singleton& Instance()
  {
    static Singleton S; // no dynamic allocation, it's unnecessary
    return S;
  }
};

// Invocation
Singleton::DoSomething(i);

另一种设计有些相似,但我更喜欢它,因为它使向非全局设计的过渡变得更加容易。

class Monoid
{
public:
  Monoid()
  {
    static State S;
    state = &s;
  }

  void doSomething(int i)
  {
    state->count += i;
  }

private:
  struct State
  {
    int count;
  };

  State* state;
};


// Use
Monoid m;
m.doSomething(1);

这里的净优势是状态的“全局性”是隐藏的,它是客户无需担心的实现细节。对缓存非常有用。

让我们,你会质疑设计:

  • 你真的需要强制执行奇异性吗?
  • 你真的需要在main开始之前构建对象吗?

奇异性通常被过分强调。C++0x 在这里会有所帮助,但即便如此,技术上强制执行奇点而不是依赖程序员来表现自己可能会非常烦人......例如在编写测试时:你真的想在每个单元测试之间卸载/重新加载你的程序吗?只是为了改变每一个之间的配置?啊。更简单地实例化一次并相信你的程序员同事......或功能测试;)

第二个问题是技术性的,而不是功能性的。如果您确实需要在程序的入口点之前进行配置,那么您可以在程序启动时简单地阅读它。

听起来可能很幼稚,但实际上在库加载期间计算存在一个问题:您如何处理错误?如果抛出,则不会加载库。如果你不扔并继续,你就处于无效状态。不是那么好笑,是吗?一旦真正的工作开始,事情就会简单得多,因为您可以使用常规的控制流逻辑。

如果您考虑测试状态是否有效......为什么不简单地在您要测试的地方构建所有内容?

最后,问题global在于引入的隐藏依赖项。当依赖隐含地推断执行流程或重构的影响时,情况会好得多。


编辑

关于初始化顺序问题:保证单个翻译单元中的对象按照定义的顺序进行初始化。

因此,根据标准,以下代码是有效的:

static int foo() { return std::numeric_limits<int>::max() / 2; }
static int bar(int c) { return c*2; }

static int const x = foo();
static int const y = bar(x);

仅在引用另一个翻译单元中定义的常量/变量时,初始化顺序才是一个问题。因此,static只要对象仅引用static同一翻译单元内的对象,自然就可以毫无问题地表达对象。

关于空间问题:as-if规则可以在这里创造奇迹。非正式地,该as-if规则意味着您指定一个行为并将其留给编译器/链接器/运行时来提供它,而无需关心它是如何提供的。这实际上是启用优化的原因。

因此,如果编译器链可以推断出一个常量的地址从未被取走,它可能会完全忽略该常量。如果它可以推断出几个常数总是相等的,并且再一次从不检查它们的地址,它可能会将它们合并在一起。

于 2011-01-28T15:55:35.120 回答
2

是的,如果它简化了您的问题,您可以使用Construct On First Use Idiom。它总是比初始化依赖于其他全局对象的全局对象好。

另一种选择是Singleton Pattern。两者都可以解决类似的问题。但是您必须决定哪个更适合情况并满足您的要求。

据我所知,没有什么比这两种方法“更好”了。

于 2011-01-28T15:14:32.260 回答
0

单例和全局对象通常被认为是邪恶的。最简单和最灵活的方法是在你的main函数中实例化对象并将这个对象传递给其他函数:

void doSomething(const Helper& h);
int main() {
  const Parameters params(...);
  const Helper h(params);
  doSomething(h);
}

另一种方法是使辅助函数成为非成员。也许他们根本不需要任何状态,如果他们需要,你可以在调用它们时传递一个有状态的对象。

我认为没有什么可以反对常见问题解答中提到的本地静态成语。它很简单并且应该是线程安全的,如果对象不是可变的,它也应该很容易模拟并且不会引入远处的动作。

于 2011-01-28T15:02:06.220 回答
0

运行前是否Helper需要存在main?如果不是,则将(一组?)全局指针变量初始化为0. 然后使用 main 以确定的顺序使用恒定状态填充它们。如果您愿意,您甚至可以制作帮助函数来为您解引用。

于 2011-01-28T16:03:41.043 回答