与其他语言不同,C ++允许静态数据为任意类型,而不仅仅是普通旧数据。普通旧数据初始化很简单(编译器只是将值写入数据段中的适当地址),但其他更复杂的类型则不是。
如何在C ++中实现非POD类型的初始化?特别是,第一次执行函数foo
时到底发生了什么?有哪些机制用于跟踪str
是否已经初始化?
#include <string>
void foo() {
static std::string str("Hello, Stack Overflow!");
}
答案 0 :(得分:4)
C ++ 11要求函数local static
变量的初始化是线程安全的。因此,至少在兼容的编译器中,通常会使用某种同步原语,每次输入函数时都需要对其进行检查。
例如,这是该程序代码的assembly listing:
#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 :(得分:1)
我见过的实现使用隐藏的布尔变量来检查变量是否已初始化。现代编译器可以安全地执行此操作,但是IIRC,一些较旧的compilerd没有这样做,如果它同时从多个线程调用,你可以得到两次调用的构造函数。
有些事情:
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();
答案 2 :(得分: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;
}
}
答案 3 :(得分:0)
您可以通过生成汇编列表来检查编译器的功能。
MSVC2008在调试模式下生成此代码(不包括异常处理prolog / epilog等):
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
引用的静态变量,检查它是否与&amp; 1为零。如果不是,则分支到标签$LN1@foo:
。否则它或者在标志的1中,在已知位置构造字符串,然后使用'atexit'在程序出口处为其析构函数添加一个调用。然后正常继续该功能。