You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

预编译头中Nifty Counter(Schwartz Counter)失效问题排查

Nifty Counter (Schwartz Counter)在Visual Studio预编译头下的失效问题

这绝对是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的逻辑:

  1. 当Foo.h被包含在stdafx.h中时,那个静态的FooInitializer fooInitializer会被归到预编译头的编译单元里。VS会把预编译头中的静态初始化器的执行,推迟到所有非预编译头的编译单元的静态对象初始化之后。
  2. 而Bar.cpp里的BarInitializer属于普通编译单元,它的初始化会先于预编译头里的FooInitializer执行。这就导致Bar先构造,此时Foo还没被初始化,foo.doSomething()里的断言自然炸了。
  3. 一旦你把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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:33:06