预编译头中Nifty Counter(Schwartz Counter)失效问题排查
这绝对是Visual Studio预编译头和Nifty Counter idiom碰撞出来的经典坑,我来帮你把问题拆明白,再给你几个可行的解决办法:
问题复现
你本来想用Nifty Counter解决静态对象初始化顺序混乱的问题,但发现只要把包含静态初始化器的头文件(比如Foo.h)塞进VS的预编译头stdafx.h里,这个技巧直接失效——调试时能明显看到Foo的构造函数居然比Bar的还晚执行,结果Bar构造时调用foo.doSomething()直接触发断言失败。而只要把Foo.h从预编译头里拿掉,或者在#include "stdafx.h"之后手动补个静态初始化器的声明,功能就立马正常了。
你提供的复现代码如下:
Foo.cpp
#include "stdafx.h" #include "Foo.h" #include <cassert> #include <iostream> #ifdef NIFTY static int SchwartzCounter; // zero initialized at load time static typename std::aligned_storage<sizeof(Foo), alignof (Foo)>::type foo_buf; Foo& foo = reinterpret_cast<Foo&>(foo_buf); #else Foo foo; #endif Foo::Foo() { std::cout << __func__ << std::endl; } Foo::~Foo() { std::cout << __func__ << std::endl; } void Foo::doSomething() { std::cout << __func__ << std::endl; assert(x == 42); } #ifdef NIFTY FooInitializer::FooInitializer() { std::cout << __func__ << std::endl; if (SchwartzCounter++ == 0) { new (&foo) Foo(); } } FooInitializer::~FooInitializer() { std::cout << __func__ << std::endl; if (--SchwartzCounter == 0) { (&foo)->~Foo(); } } #endif
Foo.h
#pragma once class Foo { public: Foo(); ~Foo(); void doSomething(); private: int x = 42; }; #ifdef NIFTY extern Foo& foo; static struct FooInitializer { FooInitializer(); ~FooInitializer(); } fooInitializer; #else extern Foo foo; #endif
Bar.cpp
#include "stdafx.h" #include "Foo.h" #include "Bar.h" #include <cassert> #include <iostream> #ifdef NIFTY static int SchwartzCounter; // zero initialized at load time static typename std::aligned_storage<sizeof(Bar), alignof (Bar)>::type bar_buf; Bar& bar = reinterpret_cast<Bar&>(bar_buf); #else Bar bar; #endif Bar::Bar() { std::cout << __func__ << std::endl; foo.doSomething(); } Bar::~Bar() { std::cout << __func__ << std::endl; } void Bar::doSomething() { std::cout << __func__ << std::endl; assert(x == 42); } #ifdef NIFTY BarInitializer::BarInitializer() { std::cout << __func__ << std::endl; if (SchwartzCounter++ == 0) { new (&bar) Bar(); } } BarInitializer::~BarInitializer() { std::cout << __func__ << std::endl; if (--SchwartzCounter == 0) { (&bar)->~Bar(); } } #endif
Bar.h
#pragma once class Bar { public: Bar(); ~Bar(); void doSomething(); private: int x = 42; }; #ifdef NIFTY extern Bar& bar; static struct BarInitializer { BarInitializer(); ~BarInitializer(); } barInitializer; #else extern Bar bar; #endif
stdafx.h
#pragma once #include "targetver.h" #include <stdio.h> #include <tchar.h> // If the following define is commented out, the nifty counter idiom is not used #define NIFTY // If the following include is commented out while the nifty counter idiom is used, the initialization order is correct #include "Foo.h"
问题根源
VS的预编译头机制(stdafx.h)是为了加速编译,把预编译头里的代码提前编译成缓存文件复用,但它对静态对象的初始化时机有特殊处理,这直接打乱了Nifty Counter的逻辑:
- 当
Foo.h被包含在stdafx.h中时,那个静态的FooInitializer fooInitializer会被归到预编译头的编译单元里。VS会把预编译头中的静态初始化器的执行,推迟到所有非预编译头的编译单元的静态对象初始化之后。 - 而
Bar.cpp里的BarInitializer属于普通编译单元,它的初始化会先于预编译头里的FooInitializer执行。这就导致Bar先构造,此时Foo还没被初始化,foo.doSomething()里的断言自然炸了。 - 一旦你把
Foo.h从预编译头移除,或者手动在每个.cpp里补初始化器声明,每个编译单元都会有自己的FooInitializer实例,这些实例的初始化会和当前单元的其他静态对象同步,Nifty Counter的计数逻辑就能正常工作,保证Foo在第一次被用之前就完成初始化。
解决办法
根据你的测试结果,这几个方案都靠谱:
方案1:把Nifty Counter头文件移出预编译头
直接把#include "Foo.h"从stdafx.h里删掉,让每个需要用Foo的.cpp自己包含Foo.h。这样每个编译单元都会生成自己的FooInitializer实例,初始化时机完全符合Nifty Counter的设计,顺序问题直接解决。方案2:在每个使用的.cpp里手动补初始化器
如果因为编译性能必须把Foo.h留在预编译头,那就在每个包含stdafx.h的.cpp文件里(比如Bar.cpp),在#include "stdafx.h"之后加上static FooInitializer fooInitializer;。这样每个编译单元都有自己的初始化器,确保当前单元的静态对象初始化前就触发Foo的构造。方案3:调整VS预编译头设置
可以试试修改项目属性:项目属性 -> C/C++ -> 预编译头,要么调整预编译头输出的相关设置,要么干脆禁用预编译头(如果编译速度能接受的话)。不过这个方案影响面大,需要权衡性能和功能。
内容的提问来源于stack exchange,提问作者sigy

