【问题标题】:Is static object guaranteed to be initialized静态对象是否保证被初始化
【发布时间】:2013-09-03 19:30:16
【问题描述】:

我正在尝试了解静态对象的初始化。假设您了解常量表达式和constexpr,静态初始化似乎很简单。动态初始化似乎有点棘手。

[basic.start.init]/4

是否在 main 的第一条语句之前完成具有静态存储持续时间的非局部变量的动态初始化是实现定义的。如果初始化延迟到 main 的第一个语句之后的某个时间点,它应该发生在与要初始化的变量在同一翻译单元中定义的任何函数或变量的第一次 odr-use (3.2) 之前。

脚注 34

具有静态存储持续时间且具有副作用的初始化的非局部变量必须被初始化,即使它没有被 odr 使用(3.2、3.7.1)。

[basic.start.init]/5

在线程的初始函数的第一个语句之前是否完成具有静态或线程存储持续时间的非局部变量的动态初始化是由实现定义的。如果初始化延迟到线程初始函数的第一个语句之后的某个时间点,它应该发生在任何变量的第一次 odr-use (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 是否被使用的问题。鉴于s 是翻译单元中唯一的对象,是否保证main 将返回1?

【问题讨论】:

    标签: c++ c++11


    【解决方案1】:

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

    总结一下我的解释:具有静态存储持续时间和动态初始化的非局部变量将:

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

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

    当一个 TU 中的任何东西被 ord-used 时,每个具有静态存储持续时间且具有副作用的初始化的非局部变量都必须被初始化,即使是未使用 odr-used 的变量。

    所以,如果有一个 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("%d\n", count);
    }
    

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

    自然,理智的编译器会在 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("%d\n", count);
        dlopen("./h2.so", RTLD_NOW);
        printf("%d\n", 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++),其中加载库的系统调用将由编译器在第一次时自动生成它是必需的。

    【讨论】:

    • 我同意这可能是为了处理动态库。这听起来像是除非您从每个翻译单元中显式调用某些内容,否则无论多么不可能,来自某个翻译单元的静态变量都可能不会被初始化。
    • 我不同意你对脚注 34 的解释; [basic.stc.static]/2 说“如果具有静态存储持续时间的变量具有初始化或具有副作用的析构函数,即使它看起来未使用 [...] 也不应被消除”所以它必须是即使它的 TU 没有任何内容是 odr-used。我不认为它的意图可能是强制它的存在(即使没有使用 odr),而不是它的初始化(如果它的 TU 没有任何东西是 odr-used)。
    • @DyP: [basic.stc.static]/2 禁止编译器消除已初始化的变量,即使它未使用。但是 [basic.start.init]/4 允许无限期地延迟其初始化,如果 TU 没有被 odr 使用(我认为 elimination 是一个优化术语,顺便说一句)。因此,如果 TU 未被使用,则该变量必须存在,但它可能在整个程序运行期间未初始化。并且应用原样规则,就好像变量根本不存在一样!
    【解决方案2】:

    如果不在定义中搜索正确的页面,我可以说你的程序保证返回 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 文件),则结果未定义。

    【讨论】:

    • 实际上在同一个编译单元中全局变量之间的顺序是有保证的。但是,没有指定跨多个编译单元的排序。
    • 真的吗?不知道那个。我在一个项目中遇到了这个问题,所以我的读物是经典的“不要这样做”。像这样改变新信息的答案是否合法?
    • 3.6.2 说“在单个翻译单元中定义的有序初始化变量应按照它们在翻译单元中的定义顺序进行初始化。”而“有序初始化”包括一些模板成员变量,以及所有非模板。
    • 当然你可以更新你的答案。只需添加更多翻译单元(文件)。
    • 是的。我弄错了静力学。我不应该这么晚才研究stackoverflow :-(。我纠正了这一点。
    【解决方案3】:

    “在与要初始化的变量相同的翻译单元中定义的任何函数或变量的第一次 odr 使用”包括要初始化的变量。

    【讨论】:

    • S s; 的定义中是s odr-used?如果不是,是否必须在将count 的值复制到要从main 返回的临时值之前对其进行初始化?
    • 是的,但只有 count 是 odr 使用的,它是静态初始化的,并且在与 s 不同的翻译单元中。
    • @DyP:s 的初始化由 main() 的 odr 使用触发。 count 的初始化是由它自己的 odr 使用触发的(在 S::S() 期间)。
    • "s 的初始化是由 通过 main() 的 odr-use 触发的" 嗯,这可以解释。但是在哪里指定 main 是 odr 使用的?名称main 没有出现在OP 示例中的表达式中,所以有什么特殊规则吗?
    • 如果 main() 被 odr-used,那么问题仍然存在,但 s 已移至另一个翻译单元。
    猜你喜欢
    • 1970-01-01
    • 2018-01-17
    • 1970-01-01
    • 1970-01-01
    • 2013-04-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多