是否保证初始化静态对象

Is static object guaranteed to be initialized

本文关键字:静态 对象 初始化 是否      更新时间:2023-10-16

我正在尝试了解静态对象的初始化。假设您理解常量表达式和constexpr,那么静态初始化似乎非常直接。动态初始化似乎有点棘手。

[basic.start.init]/4

是否在主语句的第一个语句之前完成具有静态存储持续时间的非局部变量的动态初始化,这是一个实现定义。如果初始化推迟到主语句的第一次语句之后的某个时间点,则应在第一次使用(3.2)与要初始化的变量在同一翻译单元中定义的任何函数或变量之前进行。

脚注34

具有静态存储持续时间的非局部变量在初始化时具有副作用,即使未使用,也必须进行初始化(3.2,3.7.1)

[basic.start.init]/5

它的实现定义了是否在线程的初始函数的第一个语句之前完成具有静态或线程存储持续时间的非局部变量的动态初始化。如果初始化被推迟到线程初始函数的第一个语句之后的某个时间点,则应在第一次使用(3.2)任何变量之前进行,该变量的线程存储持续时间定义为与要初始化的变量相同的转换单位。

我假设"线程的初始函数"指的是main,而不仅仅是以std::thread开头的线程。

h1.h

#ifndef H1_H_
#define H1_H_
extern int count;
#endif

tu1.cpp

#include "h1.h"
struct S
{
   S()
   {
      ++count;
   }
};
S s;

tu2.cpp

#include "h1.h"
int main(int argc, char *argv[])
{
   return count;
}

tu3.cpp

#include "h1.h"
int count;

因此,如果编译器推迟动态初始化,那么脚注34似乎指出s必须在某个时刻初始化。由于在转换单元中没有其他具有动态初始化的变量,因此没有其他变量可用于odr强制初始化tu1中的变量。s在什么时候保证已经初始化?

main是否保证返回1?此外,是否有办法更改此程序,使其不再保证返回1?或者,如果不能保证,有没有办法改变这个程序,让它变得有保障?


我分解了代码,使s的定义与main的翻译单元不同。这避免了main是否被odr使用的问题。假设s是翻译单元中唯一的对象,是否保证main会返回1?

我认为所有这些措辞都是为了描述动态加载库中会发生什么,但没有明确命名它们。

总结一下我是如何解释的:一个具有静态存储持续时间和动态初始化的非本地变量将:

  1. 在第一次使用其翻译单元中的任何内容之前进行初始化
  2. 可能,在启动main之前,但可能在它之后

我将脚注34解释为(但请记住,脚注不是规范性的):

当使用TU中的任何内容时,必须初始化每个具有静态存储持续时间的非局部变量,即使是未使用的变量。

因此,如果有一个TU没有使用任何单词,那么它的动态初始化可能不会发生。

示例

h1.h

extern int count;
struct S
{
    S();
};

h1.cpp

#include "h1.h"
int count;
S::S()
{
   ++count;
}

h2.cpp

#include "h1.h"
S s;

main.cpp

#include "h1.h"
#include <stdio.h>
int main()
{
    printf("%dn", count);
}

这可能会打印0或1:由于TU h2中的任何内容都从未使用过odr,因此未指定何时完成s的代码初始化(如果有的话)。

自然,sane编译器会在main之前初始化s,所以它肯定会打印1:

$ g++ main.cpp h2.cpp h1.cpp -o test1
$ ./test1
1

现在,假设h2.cpp在一个共享库中:

$ g++ -shared -fPIC h2.cpp -o h2.so

现在的主文件是:

main2.cpp

#include "h1.h"
#include <dlfcn.h>
#include <stdio.h>
int main()
{
    printf("%dn", count);
    dlopen("./h2.so", RTLD_NOW);
    printf("%dn", count);
    return 0;
}

编译并运行:

$ g++ -shared -fPIC h2.cpp -o h2.so
$ g++ -rdynamic main.cpp h1.cpp -ldl -o test2
$ ./test2
0
1

看到了吗?s的初始化已延迟!好的一点是,如果不首先加载它,就不可能引用动态加载库中的任何内容,并且加载它将触发动态初始化。所以一切都很好。

如果你认为使用dlopen是作弊,请记住,有些编译器支持延迟加载共享库(例如VC++),在第一次需要时,编译器会自动生成加载库的系统调用。

如果不在定义中搜索正确的页面,我可以说您的程序保证返回1。每次静态或全局初始化都在main中的第一个命令之前完成。首先初始化全局变量,然后执行全局对象的构造函数。函数/方法范围内的静态函数在首次使用前已初始化。但有一个陷阱:

int count;
struct A
{
   A()
   {
     count=5;
   }
};
struct B
{
   B()
   {
     count=count*2;
   }
};

A a;
B b;
void main(void)
{
  return count;
}

正如Ben Voigt的一篇评论中所提到的,如果两个实例都是在同一个翻译单元中创建的,则会定义结果。所以在我的样本中,结果是10。如果实例是在不同的文件中创建的(并分别编译为不同的.obj文件),则不会定义结果。

"首次使用与待初始化变量定义在同一翻译单元中的任何函数或变量"包括待初始化变量。