gcc中模板的非推迟静态成员初始化?

12

gcc是否保证静态成员初始化的时间,特别是涉及模板类时?

我想知道是否可以得到一个硬性保证,即在多个编译单元实例化类时,静态成员(PWrap_T<T>::p_s)将在main()之前初始化。尝试在每个编译单元中手动触摸符号以在main开始时启动不切实际,但我不确定是否有其他方法。

我已经使用了不同单元中的方法bar()进行了测试,并且始终获得了所需的结果,但我需要知道是否会有任何情况下gcc会出现问题,以及是否可以防止此类情况发生。

此外,DSO中的所有静态成员是否都会在库加载完成之前初始化?

#include <iostream>
#include <deque>

struct P;
inline std::deque<P *> &ps() { static std::deque<P *> d; return d; }
void dump();

struct P {
  P(int id, char const *i) : id_(id), inf_(i) { ps().push_back(this); }
  void doStuff() { std::cout << id_ << " (" << inf_ << ")" << std::endl; }
  int  const        id_;
  char const *const inf_;
};

template <class T>
struct PWrap_T { static P p_s; };

// *** Can I guarantee this is done before main()? ***
template <class T>
P PWrap_T<T>::p_s(T::id(), T::desc());

#define PP(ID, DESC, NAME) /* semicolon must follow! */  \
struct ppdef_##NAME  {                                   \
  constexpr static int         id()   { return ID; }     \
  constexpr static char const *desc() { return DESC; }   \
};                                                       \
PWrap_T<ppdef_##NAME> const NAME

// In a compilation unit apart from the template/macro header.
void dump() {
  std::cout << "[";
  for (P *pp : ps()) { std::cout << " " << pp->id_ << ":" << pp->inf_; }
  std::cout << " ]" << std::endl;
}

// In some compilation unit.
void bar(int cnt) {
  for (int i = 0; i < cnt; ++i) {
    PP(2, "description", pp);
    pp.p_s.doStuff();
  }
}

int main() {
  dump();
  PP(3, "another", pp2);
  bar(5);
  pp2.p_s.doStuff();
}

(C++11 §3.6.2/4 - [basic.start.init] :))非局部静态存储期变量的动态初始化是否在main函数的第一条语句之前完成是由实现定义的。如果初始化被延迟到main函数的第一条语句之后的某个时间点,则应在与要初始化的变量定义在同一翻译单元中的任何函数或变量的第一次odr-use(3.2)之前发生,否则行为未定义。... 具有具有副作用的初始化的静态存储期非局部变量必须进行初始化,即使它没有被odr-used(3.2, 3.7.1)。 此外,尝试使用__attribute__ ((init_priority(int)))或__attribute__ ((constructor))对模板成员的初始化产生了警告:attributes after parenthesized initializer ignored,并且我不知道其他关于静态初始化的技巧。感谢提供答案的任何人!

我想odr-use规则的意图是覆盖可能具有文件作用域对象的动态共享对象(DSO)。如果通过dlopen()在main函数启动后引入DSO,则显然无法初始化DSO中的所有内容,但理论上dlopen()可以确保在调用DSO中的任何其他内容之前初始化DSO中的所有内容。我想最终的答案取决于您编译的操作系统/架构的ABI定义。 - Joe Z
单例模式解决了这个问题,不是吗? - lkanab
2个回答

3

该标准保证静态存储期对象在与您的对象在同一编译单元中的任何函数/变量从外部源使用之前进行初始化。

这里的措辞是为了与共享库配合使用而设计的。因为共享库可以在main()启动后动态加载,所以语言规范必须足够灵活以应对它。但只要您从翻译单元外部访问您的对象,就保证在给予访问之前它已经被构造(除非您正在执行某些病态操作)。

但是,如果它在同一编译单元中的另一个静态存储期对象的构造函数中使用,则无法阻止其在初始化之前使用。

但是,您可以通过以下技术轻松手动提供静态对象在使用之前正确初始化的保证。

只需将静态变量更改为静态函数。然后在函数内声明一个返回的静态成员。因此,您可以像以前一样使用它(只需添加())。

template <class T>
struct PWrap_T
{
    static P& p_s();  // change static variable to static member function.
                      // Rather than the type being P make it a reference to P
                      // because the object will be held internally to the function
};

template <class T>
P& PWrap_T<T>::p_s()
{
    // Notice the member is static.
    // This means it will live longer than the function.
    // Also it will be initialized on first use.
    // and destructed when other static storage duration objects are destroyed.
    static P p_s_item(T::id(), T::desc());

    return p_s_item;

    // Note its not guaranteed to be created before main().
    // But it is guaranteed to be created before first use.
}

在这里,你能获得全球范围内的优势(无论它们是什么)。你可以得到确保构建/销毁并且知道对象将被正确构建在使用之前。

你需要做出的唯一改变是:

void bar(int cnt) {
  for (int i = 0; i < cnt; ++i) {
    PP(2, "description", pp);
    pp.p_s().doStuff();
     //   ^^  Add the braces here.
  }
}

唯一的问题是,在 C++11 之前,函数静态变量不是线程安全的(至少在它们的初始化过程中)。 - Bwmat
@Bwmat:确实,在C++11之前,该语言并不保证该功能。但是gcc在C++11之前就已经实现了该功能。 - Martin York
@Loki,您是正确的,这是一种保证安全的方法,但我的问题是如果可能的话既能够安全又能够尽早完成静态初始化。您使用的Meyers单例方法不允许我在main()之前初始化和枚举所有P实例,这才是我的真正目标。这些对象的构造成本非常高,并且在运行时的开头生成实例的完整列表将非常有用(以及避免对列表进行运行时锁定以进行添加)。 - Jeff
@Jeff:正如前两段所指出的那样,你的方法应该是没问题的。唯一的问题是当你尝试从其他静态存储期对象的构造函数/析构函数中访问静态存储期对象时。这就是你需要使用上述技术的时候。 - Martin York

1
你已经发现了C++标准没有保证“具有静态存储期的非本地变量的动态初始化在 main 函数的第一条语句之前完成”。然而,GCC会在执行main函数之前执行此类初始化,如处理初始化函数中所述。
唯一的问题是使用dlopen加载的共享库中静态对象的初始化。这些只会在加载库的时候初始化,但你无能为力。

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