非POD静态值如何初始化?

6
C++与其他语言不同的是,允许静态数据为任意类型,而不仅仅是简单的旧数据。初始化简单的旧数据很容易(编译器只需在数据段中适当的地址写入值),但其他更复杂的类型则不然。
C++通常如何实现非POD类型的初始化?特别地,在执行函数foo时第一次发生了什么?使用哪些机制来跟踪str是否已经被初始化?
#include <string>
void foo() {
    static std::string str("Hello, Stack Overflow!");
}

实现细节。一个可能的变体是将函数指针移动到初始化之后。 - sp2danny
@sp2danny 我怀疑这是一个实现细节。这就是为什么我问它通常是如何实现的。:) 关于移动函数指针:我想过那个,但我认为更可能的是在函数开头插入一条跳转指令,跳过初始化。这样即使在调用函数之前或没有调用函数的情况下,对函数的指针仍将有效。 - Paul Manta
4个回答

6

C++11要求对函数本地的static变量的初始化是线程安全的。因此,在遵循规范的编译器中,通常会使用某些同步原语来确保每次进入函数时都需要检查。

例如,这个程序的代码汇编清单如下:

#include <string>
void foo() {
    static std::string str("Hello, Stack Overflow!");
}

int main() {}

.LC0:
    .string "Hello, Stack Overflow!"
foo():
    cmpb    $0, guard variable for foo()::str(%rip)
    je  .L14
    ret
.L14:
    pushq   %rbx
    movl    guard variable for foo()::str, %edi
    subq    $16, %rsp
    call    __cxa_guard_acquire
    testl   %eax, %eax
    jne .L15
.L1:
    addq    $16, %rsp
    popq    %rbx
    ret
.L15:
    leaq    15(%rsp), %rdx
    movl    $.LC0, %esi
    movl    foo()::str, %edi
    call    std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, std::allocator<char> const&)
    movl    guard variable for foo()::str, %edi
    call    __cxa_guard_release
    movl    $__dso_handle, %edx
    movl    foo()::str, %esi
    movl    std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string(), %edi
    call    __cxa_atexit
    jmp .L1
    movq    %rax, %rbx
    movl    guard variable for foo()::str, %edi
    call    __cxa_guard_abort
    movq    %rbx, %rdi
    call    _Unwind_Resume
main:
    xorl    %eax, %eax
    ret
__cxa_guard_acquire__cxa_guard_release等函数用于保护静态变量的初始化过程。

1
我看到的实现使用了一个隐藏的布尔变量来检查变量是否已初始化。现代编译器会以线程安全的方式执行此操作,但是如果从多个线程同时调用它,则可能会在某些旧编译器上出现构造函数被调用两次的情况。
类似于以下内容:
static bool __str_initialized = false;
static char __mem_for_str[...]; //std::string str("Hello, Stack Overflow!");

void foo() {
    if (!__str_initialized)
    {
        lock();
        __str_initialized = true;
        new (__mem_for_str) std::string("Hello, Stack Overflow!");
        unlock();
    }
}

然后,在程序的最终化代码中:
if (__str_initialized)
     ((std::string&)__mem_for_str).~std::string();

0

这是实现特定的。

通常会有一个标志(静态初始化为零),以指示它是否已初始化,并且(在C++11或早期线程安全的实现中)一些种类的互斥锁同样是可以静态初始化的,以保护避免多个线程尝试初始化它。

生成的代码通常表现为以下行为

static __atomic_flag_type __initialised = false;
static __mutex_type __mutex = __MUTEX_INITIALISER;

if (!__initialised) {
    __lock_type __lock(__mutex);
    if (!__initialised) {
        __initialise(str);
        __initialised = true;
    }
}

看到使用其他静态变量来解释静态初始化的说明很有趣 :) - Jarod42
@Jarod42:确实如此。这就是理解C++中“静态”这个词的各种重载含义的重要性所在;特别是静态存储期和静态初始化之间的区别。 - Mike Seymour

0

您可以通过生成汇编清单来检查编译器的操作。

MSVC2008在调试模式下生成此代码(不包括异常处理前言/后言等):

    mov eax, DWORD PTR ?$S1@?1??foo@@YA_NXZ@4IA
    and eax, 1
    jne SHORT $LN1@foo
    mov eax, DWORD PTR ?$S1@?1??foo@@YA_NXZ@4IA
    or  eax, 1
    mov DWORD PTR ?$S1@?1??foo@@YA_NXZ@4IA, eax
    mov DWORD PTR __$EHRec$[ebp+8], 0
    mov esi, esp
    push    OFFSET ??_C@_0BH@ENJCLPMJ@Hello?0?5Stack?5Overflow?$CB?$AA@
    mov ecx, OFFSET ?str@?1??foo@@YA_NXZ@4V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A
    call    DWORD PTR __imp_??0?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAE@PBD@Z
    cmp esi, esp
    call    __RTC_CheckEsp
    push    OFFSET ??__Fstr@?1??foo@@YA_NXZ@YAXXZ   ; `foo'::`2'::`dynamic atexit destructor for 'str''
    call    _atexit
    add esp, 4
    mov DWORD PTR __$EHRec$[ebp+8], -1
$LN1@foo:

即存在一个静态变量被引用,其名称为?$S1@?1??foo@@YA_NXZ@4IA,检查它是否与1相等。如果不是,则跳转到标签$LN1@foo:。否则,将1或运算到标志中,在已知位置构造字符串,并在程序退出时添加其析构函数的调用,使用'atexit'。然后继续正常执行函数。


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