初始化顺序是否有保证?

4
我正在使用类似以下代码段进行一些初始化操作。 我知道p<T> :: i_的初始化是无序的。 我相信h是有序的,因此我应该能够合理地推断它被初始化的顺序。 鉴于在定义h之前已经包含了p的头文件,是否有保证p<T> :: i_将在h之前初始化?
struct helper
{
   template <typename T>
   helper(const T&, int i)
   {
      p<T>::i_::push_back(i);
   }
};
static helper h;

以下定义了类p。

template <typename T>
struct p
{
   static std::vector<int> i_;
};
template <typename T>
std::vector<int> p<T>::i_;

这个无法编译 - helper 没有一个默认的构造函数。 - Igor Tandetnik
用哪个构造函数来初始化 h - David Rodríguez - dribeas
3
我认为 h 是有序的。相对于什么是有序的呢? - jrok
3个回答

6
具有静态存储期的对象初始化顺序在翻译单元之间是未定义的,在每个翻译单元内是按顺序进行的。
在您的特定情况下,情况更加复杂,因为具有静态存储的对象之一是模板类的静态成员。这实际上意味着访问成员 p<T>::i_ 的每个翻译单元将创建该符号,并添加适当的初始化代码。稍后,链接器将选择一个实例并保留它。即使看起来像 p<T>::i_ 在您的翻译单元中定义于 h 之前,您也不知道链接器将保留哪个 p<T>::i_ 实例,可能是另一个翻译单元中的实例,因此顺序不能保证。
通常拥有全局对象是一个不好的主意,建议您尝试重新设计您的程序,避免使用这些全局变量。

没错,我正尝试理解这个问题。也许我的措辞不太准确。在 h::helper<ConcreteType>() 实例化期间,p<ConcreteType> 不会被实例化吗?然后 p<ConcreteType>::i_ 就会被初始化了?也许我应该直接尝试一下。 - lapk
关于跨翻译单元的初始化顺序,我认为在翻译单元中调用函数可以保证在进入函数之前对其中的静态持续对象进行初始化,因此在函数调用初始化器中采取简单的预防措施可以使生活变得轻松。 (尽管您可以尝试使不可能的排序要求,如果在A中对a进行初始化,则在A中使用y()调用x()的全局初始化调用B中的a是最简单的病态之一) - jthill
1
@PetrBudnik:是的,在构造函数调用h内部的调用将导致p<ConcreteType>::i_的实例化,但那不一定是p<ConcreteType>::i_唯一实例化。如果另一个翻译单元包含了helper的克隆类型helper2,并且它创建了一个具有静态存储期的变量h2,触发了p<ConcreteType>::i_的实例化,那么两个翻译单元都会实例化它。链接器将删除其中一个。对于保留自己实例的翻译单元,顺序是有保证的,但对于另一个翻译单元,则没有保证[...] - David Rodríguez - dribeas
1
[...] 这将会删除 helper 中的版本,最终顺序可能是 h > p<CT>::i_ > h2。在初始化 h 时,尚未初始化的 p<CT>::i_ 将被访问,导致未定义的行为。 - David Rodríguez - dribeas
@jthill:那个句子(3.6.2/4)的开头是如果初始化被推迟到main函数的第一条语句之后的某个时间点。引用限制了初始化可以被推迟多少,但这并不能保证具有静态存储的不同变量的初始化顺序。这是可以强制执行的。考虑两个翻译单元,分别定义了函数fafb以及初始化为T a = fb();U b = fa();的变量ab - David Rodríguez - dribeas
显示剩余7条评论

5

全局或命名空间作用域的对象在一个翻译单元中从上到下构造。不同翻译单元之间全局或命名空间级别的构造顺序未定义。在翻译单元之间进行初始化排序的最合理方法是将对象包装在适当的访问器函数中,例如:

template <typename T>
something<T>& get() {
    static something<T> values;
    return value;
}

请注意,这在C++03中不是线程安全的(因为C++03本身没有线程的概念)。但在C++11中是线程安全的。


0

不,这并不是保证。

但你可以做的是:

template<typename T>
std::vector<int>& registry() {
    static std::vector<int> reg;
    return reg;
}

...
registry<T>().push_back(i);
...

更好的方法是在启动期间避免做过于智能的事情。
在main函数开始之前或结束之后进行调试是一场真正的噩梦(而且我认为标准甚至没有覆盖到100%)。简单的注册可能还可以,但绝不要做任何可能失败的事情。
多年来,我已经放弃了这种方法,转而采用显式初始化/关闭,并且从未回头。

调试动态初始化可能没有你想象的那么难。有一个平台相关的符号可以打断点,标记动态初始化的入口,然后您可以像任何函数一样逐步执行它。 - Andrew Tomazos
@user1131467:我发现在初始化和关闭期间,调试工具的效果并不如预期。需要注意的是,在初始化期间,甚至不清楚标准库的多少已经被初始化并可以使用,以及静态实例销毁期间标准库的多少已经被关闭。此外,在某些情况下,例如在退出应用程序时,Windows会悄悄地吞噬段错误... - 6502

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