C++模板:全局对象中的静态成员未被初始化

12

我有一段简单的C++代码,其中我定义了一个模板和一个全局对象,通过对模板进行特化来定义该对象。对象构造函数访问特定模板中的静态成员变量,但事实证明,在这一点上静态成员未被初始化。但对于一个局部对象(在函数体内定义),它可以正常工作。我感到很困惑...

我的C++编译器是:g++(Ubuntu 5.4.0-6ubuntu1~16.04.4)5.4.0 20160609

/////////////////////////
template<typename T>
class TB{
public:
  const char *_name;
  TB(const char * str):_name(str){
    cout << "constructor is called:" << _name << endl;
  };

  virtual ~TB(){
    cout << "destructor is called:" << _name << endl;
  };
};

template<typename T>
class TA{
public:
  const char *_name;
  TA(const char * str):_name(str){
    cout << "constructor is called:" << _name << endl;
    cout << tb._name <<endl;
  };

  virtual ~TA(){
    cout << "destructor is called:" << _name << endl;
  };

  static TB<T> tb;
};

template<typename T>
  TB<T> TA<T>::tb("static-tb");
TA<int> ta("global-ta");

int main(int argc,char ** argv){
  cout << "program started." << endl;
  cout << "program stopped." << endl;
  return 0;
}

/////////////////////////
//  OUTPUT:
constructor is called:global-ta
// yes, only such a single line.

如果我将ta的定义放在main()函数中,像下面这样,它就可以工作。

int main(int argc,char ** argv){
  cout << "program started." << endl;
  TA<int> ta("local-ta");
  cout << "program stopped." << endl;
  return 0;
}

/////////////////////
//  OUTPUT:
constructor is called:static-tb
program started.
constructor is called:local-ta
static-tb
program stopped.
destructor is called:local-ta
destructor is called:static-tb
// end of output

你可以尝试使用其他编译器,比如Clang或GCC 7吗? - John Zwinck
cout 改为 printf,查看输出。TB<T> TA<T>::tb("static-tb"); 没有被实例化,请参考 https://dev59.com/ClnUa4cB1Zd3GeqPfvo-?rq=1。 - Liu Hao
为什么甚至连“程序已启动”都没有被打印出来? - suzuiyue
@JohnZwinck我可以在VS2015中重现它。 - A.S.H
1个回答

9
在第一种情况下,你的程序在主函数开始之前崩溃了。它在ta的构造函数内部崩溃,因为它访问了尚未构造的tb。这是一种静态初始化顺序混乱的形式。
第二种情况是成功的,因为tamain内部,这保证了非局部的tbta之前被构造。
问题是,为什么在第一种情况下,即使tbta在同一编译单元中定义,并且tbta之前定义,ta仍然在tb之前被构造?
知道tb是一个模板静态数据成员,cppreference上的这个引用适用于此

动态初始化

当所有静态初始化完成后,以下情况会发生非局部变量的动态初始化:

1)无序动态初始化,仅适用于(静态/线程本地)变量模板和(自 C++11 以来)未显式专门化的类模板静态数据成员。这些静态变量的初始化与除了程序在变量初始化之前启动线程之外的所有其他动态初始化的顺序不确定(自 C++17 起,如果程序在变量初始化之前启动线程,则其初始化是无序的)。这些线程本地变量的初始化与所有其他动态初始化的顺序不确定。

因此,在这里没有保证顺序!由于 ta 是具有 显式 模板专门化的静态变量,编译器可以在 tb 之前初始化它。

同一页面上的另一个引用如下:

早期动态初始化

如果以下两个条件都成立,编译器允许在静态初始化时(基本上是在编译时)作为动态初始化的一部分来初始化动态初始化变量:

1)动态初始化版本在其初始化之前不会更改任何命名空间范围内的其他对象的值

2)如果所有不需要静态初始化的变量都被动态初始化,则静态初始化版本将在初始化变量时产生与动态初始化产生的相同值。由于上述规则,如果某个对象 o1 的初始化引用了一个命名空间范围对象 o2,该对象可能需要动态初始化,但在同一翻译单元中稍后定义,那么使用的 o2 值是完全初始化的 o2 的值(因为编译器将 o2 的初始化提升到编译时),还是仅零初始化的 o2 的值是未指定的。

根据这些规则,编译器决定在 tb 之前提升初始化 ta。无论它是否被提升为静态初始化都不清楚,但无论如何,对于变量模板和静态模板成员,初始化顺序似乎是不被保证的,这是由于第一条引言和第二条引言的提升规则造成的。

解决方案

为了确保在使用之前初始化tb,最简单的方法是将其放在包装函数内部。我认为,在处理静态模板成员时,这应该是一个经验法则。
template<typename T>
class TA{
    //...
    static TB<T>& getTB();
};

template<typename T>
TB<T>& TA<T>::getTB()
{ static TB<T> tb("static-tb");
  return tb;
}

“tb”不是一个变量模板;它是一个类模板的静态数据成员。此外,如果你只是从cppreference中复制粘贴,那么(since...)标记会让人感到困惑。 - T.C.
1
@A.S.H 非常感谢!这个解决方案对我很有用。此外,解释和引用也很有帮助。我觉得编译器能够捕捉到依赖关系,因此应该能够提供更好的初始化顺序。 - Weijia Song
@WeijiaSong 很高兴知道这对你有所帮助 :). 编译器在静态变量上进行依赖分析可能是可行的,但实际上似乎这个问题非常困难,特别是在使用模板时,这就是为什么标准没有规定这样的分析。个人而言,在怀疑时我会系统地使用包装函数。 - A.S.H
我刚遇到了一个类似的问题,想尝试一下你的代码,但是不太清楚你如何使用包装函数。你是否仍将TB<T> tb作为TA的成员?你是否像这样初始化它:template <typename T> TB<T> TA<T>::tb = TA<T>::getTB()?我刚试过了,它仍然只打印出“constructor is called:global-ta”。你能发一下完整正确的代码吗? - BRabbit27
1
@BRabbit27 不,类中不再有 tb 成员。tb 只是函数 getTb() 中的一个“本地”(但静态)变量。无论您的代码想要访问 tb,都可以忘记名称 tb,直接使用 getTb()。例如:TA::getTb().doSomething()。希望这能帮到您。 - A.S.H

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