魔法静态变量 vs. std::call_once
摘要
本文比较了两种 C++ 技术:魔法静态变量和 std::call_once,用于缓存计算,并解释了根据作用域和初始化等因素何时使用哪种技术。
<p>假设你有如下函数</p>
<pre>bool should_use_widgets()
{
bool supported = ⟦ complex code to check OS features ⟧;
return supported && is_configuration_enabled("widgets");
}
</pre>
<p>由于操作系统 Widget 支持在程序生命周期内不会改变,你希望只计算一次并缓存结果。</p>
<p>一种方法是使用所谓的“魔法静态变量”:</p>
<pre>bool should_use_widgets()
{
static const bool supported = [] {
return ⟦ complex code to check OS features ⟧;
}();
return supported && is_configuration_enabled("widgets");
}
</pre>
<p>函数局部静态变量在第一次执行到达变量时初始化。后续执行时,不会发生任何事情。</p>
<p>另一种方法是使用 <code>std::call_once</code>。</p>
<pre>bool is_supported_cached;
std::once_flag is_supported_once;
bool are_widgets_supported()
{
std::call_once(is_supported_once, [] {
is_supported_cached = ⟦ complex code to check OS features ⟧;
});
return is_supported_cached && is_configuration_enabled("widgets");
}
</pre>
<p>为什么你会选择其中一种而不使用另一种?</p>
<p>魔法静态变量确实更方便。你不必管理两个变量。只需声明一个函数局部 <code>static</code> 并初始化它。一个问题在于它必须是函数局部静态变量。多个函数无法访问同一个缓存变量。但这很容易解决:创建一个函数,其唯一任务就是管理那个静态变量。</p>
<pre>bool are_widgets_supported_in_os()
{
static const bool supported = [] {
return ⟦ complex code to check OS features ⟧;
}();
return supported;
}
bool are_widgets_supported()
{
return are_widgets_supported_in_os() &&
is_configuration_enabled("widgets");
}
bool are_widget_carriers_supported()
{
return are_widgets_supported_in_os() &&
is_configuration_enabled("widget_carriers");
}
</pre>
<p>这种技巧常用于单例模式。</p>
<pre>class Singleton
{
public:
static Singleton& GetInstance()
{
static Singleton instance;
return instance;
}
⟦ various methods go here ⟧;
private:
Singleton() = default;
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
~Singleton() = default;
}
</pre>
<p>那么,何时使用 <code>call_once</code>?</p>
<p>魔法静态变量仅适用于静态变量。也许你想对非静态数据成员进行延迟初始化。</p>
<p>假设我们有一个 <code>Gadget</code>,它在构造时关联一个 <code>Widget</code>。并且假设 <code>Gadget</code> 对极性反转的支持取决于 <code>Widget</code> 是否支持极性反转。此外,极性可逆性计算成本高,但由于它是不可变属性,我们可以只计算一次并缓存结果。</p>
<pre>class Gadget
{
public:
Gadget(std::shared_ptr<Widget> const& widget) : widget(widget) {}
bool can_reverse_polarity()
{
return can_reverse_polarity_cached;
}
private:
std::shared_ptr<Widget> const widget;
bool can_reverse_polarity_cached =
is_configuration_enabled("polarity_reversal") &&
is_widget_polarity_reversible(*widget);
};
</pre>
<p><code>can_<wbr />reverse_<wbr />polarity_<wbr />cached</code> 是一个非静态数据成员,具有显式初始化器,因此它在 <code>Gadget</code> 类构造时初始化,而不是在第一次有人调用 <code>can_<wbr />reverse_<wbr />polarity</code> 时按需初始化。</p>
<p>“没问题,”你说。“我可以使用魔法静态变量。”</p>
<pre> bool can_reverse_polarity()
{
<span style="border: solid 1px currentcolor; border-bottom: none;">static bool can_reverse_polarity_cached = </span>
<span style="border: 1px currentcolor; border-style: none solid;"> is_configuration_enabled("polarity_reversal") &&</span>
<span style="border: solid 1px currentcolor; border-top: none;"> is_widget_polarity_reversible(*widget); </span>
return can_reverse_polarity_cached;
}
</pre>
<p>成员函数中的函数静态变量相对于该成员函数是静态的。所有 <code>Gadget</code> 实例共享同一个成员函数,因此它们都共享同一个 <code>can_<wbr />reverse_<wbr />polarity_<wbr />cached</code> 变量。当你调用 <code>Gadget::<wbr />can_<wbr />reverse_<wbr />polarity()</code> 时,它会计算与调用它的 <code>Gadget</code> 关联的 <code>Widget</code> 的可逆性,然后该值将被锁定用于所有未来对 <code>Gadget::<wbr />can_<wbr />reverse_<wbr />polarity()</code> 的调用,即使未来的调用可能针对不相关的 <code>Gadget</code>。</p>
<p>我们想要的是魔法静态变量的一种变体,它为每个类的实例初始化,而不是对所有实例只初始化一次。</p>
<p>这就是 <code>std::<wbr />call_once</code> 的情况。</p>
<pre>class Gadget
{
public:
Gadget(std::shared_ptr<Widget> const& widget) : widget(widget) {}
bool can_reverse_polarity()
{
<span style="border: solid 1px currentcolor; border-bottom: none;">std::call_once(can_reverse_polarity_once, [] { </span>
<span style="border: 1px currentcolor; border-style: none solid;"> can_reverse_polarity_cached = </span>
<span style="border: 1px currentcolor; border-style: none solid;"> is_configuration_enabled("polarity_reversal") &&</span>
<span style="border: 1px currentcolor; border-style: none solid;"> is_widget_polarity_reversible(*widget); </span>
<span style="border: solid 1px currentcolor; border-top: none;">}); </span>
return can_reverse_polarity_cached;
}
private:
std::shared_ptr<Widget> const widget;
<span style="border: solid 1px currentcolor; border-bottom: none;">bool can_reverse_polarity_cached; // initializes on demand</span>
<span style="border: solid 1px currentcolor; border-top: none;">std::once_flag can_reverse_polarity_once; </span>
};
</pre>
<p>我想你可以将其封装在 <code>lazy<T></code> 类型中。¹</p>
<pre>template<typename T, typename L>
struct lazy
{
lazy(L&& l) : init(std::forward<L>(l)) {}
T& get() {
std::call_once(once, [&] {
value.emplace(init());
});
return *value;
}
private:
std::optional<T> value;
std::once_flag once;
std::decay_t<L> init;
};
template<typename T, typename L>
lazy<T, L> make_lazy(L&& l)
{
return { std::forward<L>(l) };
}
void test()
{
auto v = make_lazy<int>([] {
printf("Slow calculation\n");
return 42;
});
printf("Value is %d\n", v.get());
printf("Value is still %d\n", v.get());
}
</pre>
<p>但是等等,我们还有 <code>std::async</code> 带延迟执行。我们应该使用它吗?我们将在下一次探讨这个问题。</p>
<p>¹ 注意,这与 <a href="https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2506r0.pdf"> <code>std::lazy</code> 提案</a> 不同。</p>
<p>本文 <a href="https://devblogs.microsoft.com/oldnewthing/20260916-00/?p=112703">魔法静态变量 vs. <CODE>std::call_once</CODE></a> 首次发布于 <a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>。</p>
查看缓存全文
缓存时间: 2026/09/17 14:31
# 魔法静态变量 vs. std::call_once - 新旧事物之源
来源:https://devblogs.microsoft.com/oldnewthing/20260916-00?p=112703
假设你有如下函数:
```cpp
bool should_use_widgets() {
bool supported = ⟦ 复杂代码检查操作系统特性 ⟧;
return supported && is_configuration_enabled("widgets");
}
```
由于操作系统小部件支持并非程序生命周期内会发生变化的特性,你希望只计算一次并缓存结果。一种方法是使用所谓的“魔法静态变量”:
```cpp
bool should_use_widgets() {
static const bool supported = [] {
return ⟦ 复杂代码检查操作系统特性 ⟧;
}();
return supported && is_configuration_enabled("widgets");
}
```
函数内部的静态变量会在执行流首次到达该变量时初始化,后续执行时不会发生任何操作。另一种方法是使用 `std::call_once`:
```cpp
bool is_supported_cached;
std::once_flag is_supported_once;
bool are_widgets_supported() {
std::call_once(is_supported_once, [] {
is_supported_cached = ⟦ 复杂代码检查操作系统特性 ⟧;
});
return is_supported_cached && is_configuration_enabled("widgets");
}
```
为何要选择其中一种而非另一种?魔法静态变量显然更便捷——无需管理两个变量,只需声明一个函数内部 `static` 变量并初始化。但存在一个问题:它必须是函数内部的静态变量,多个函数无法访问同一个缓存变量。不过这很容易解决:创建一个专门管理该静态变量的函数:
```cpp
bool are_widgets_supported_in_os() {
static const bool supported = [] {
return ⟦ 复杂代码检查操作系统特性 ⟧;
}();
return supported;
}
bool are_widgets_supported() {
return are_widgets_supported_in_os() && is_configuration_enabled("widgets");
}
bool are_widget_carriers_supported() {
return are_widgets_supported_in_os() && is_configuration_enabled("widget_carriers");
}
```
这种技巧常用于单例模式:
```cpp
class Singleton {
public:
static Singleton& GetInstance() {
static Singleton instance;
return instance;
}
⟦ 各类方法在此实现 ⟧;
private:
Singleton() = default;
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
~Singleton() = default;
};
```
那么何时应使用 `call_once`?魔法静态变量仅适用于静态变量。若需对非静态数据成员进行懒加载初始化怎么办?假设我们有一个 `Gadget`,其构造时会关联一个 `Widget`,且 `Gadget` 的极性反转功能取决于该 `Widget` 是否支持极性反转。极性可反转性计算开销较大,但由于它是不可变属性,我们可以只计算一次并缓存结果:
```cpp
class Gadget {
public:
Gadget(std::shared_ptr<Widget> const& widget) : widget(widget) {}
bool can_reverse_polarity() {
return can_reverse_polarity_cached;
}
private:
std::shared_ptr<Widget> const widget;
bool can_reverse_polarity_cached =
is_configuration_enabled("polarity_reversal") &&
is_widget_polarity_reversible(*widget);
};
```
`can_reverse_polarity_cached` 是带有显式初始化器的非静态数据成员,会在 `Gadget` 类构造时初始化,而非在首次调用 `can_reverse_polarity` 时按需初始化。你可能会说:“没问题,我可以使用魔法静态变量。”
```cpp
bool can_reverse_polarity() {
static bool can_reverse_polarity_cached =
is_configuration_enabled("polarity_reversal") &&
is_widget_polarity_reversible(*widget);
return can_reverse_polarity_cached;
}
```
成员函数中的函数静态变量对于该成员函数而言是静态的。所有 `Gadget` 实例共享同一成员函数,因此也共享同一个 `can_reverse_polarity_cached` 变量。首次调用 `Gadget::can_reverse_polarity()` 时,它计算的是调用者所关联 `Widget` 的可反转性,而该值将固定用于后续所有 `Gadget::can_reverse_polarity()` 调用——即使后续调用可能来自无关的 `Gadget` 实例。我们需要的是一种能为每个类实例独立初始化,而非对所有实例全局初始化的魔法静态变量变体。这正是 `std::call_once` 的适用场景:
```cpp
class Gadget {
public:
Gadget(std::shared_ptr<Widget> const& widget) : widget(widget) {}
bool can_reverse_polarity() {
std::call_once(can_reverse_polarity_once, [this] {
can_reverse_polarity_cached =
is_configuration_enabled("polarity_reversal") &&
is_widget_polarity_reversible(*widget);
});
return can_reverse_polarity_cached;
}
private:
std::shared_ptr<Widget> const widget;
bool can_reverse_polarity_cached; // 按需初始化
std::once_flag can_reverse_polarity_once;
};
```
或许可以将此模式封装为 `lazy` 类型¹:
```cpp
template<typename T>
struct lazy {
lazy(L&& l) : init(std::forward(l)) {}
T& get() {
std::call_once(once, [this] { value.emplace(init()); });
return *value;
}
private:
std::optional<T> value;
std::once_flag once;
std::decay_t<L> init;
};
template<typename L>
auto make_lazy(L&& l) {
return lazy<decltype(l())>{ std::forward(l) };
}
void test() {
auto v = make_lazy([] {
printf("执行耗时计算\n");
return 42;
});
printf("值为 %d\n", v.get());
printf("值仍为 %d\n", v.get());
}
```
但请注意,我们还有 `std::async` 的延迟执行机制。是否应该采用它?我们将在下期探讨这个问题。
¹ 注意此设计与 `std::lazy` 提案(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2506r0.pdf)不同。
### 分类
### 主题
## 作者 Raymond Chen
Raymond 已参与 Windows 系统演进三十余年。2003年他创建了名为 "The Old New Thing" 的网站,其受欢迎程度远超他最狂野的想象,这一发展至今仍让他感到不可思议。该网站衍生出一本同名著作《The Old New Thing》(Addison Wesley 2007年出版)。他偶尔会出现在 Windows 开发者文档 Twitter 账号上,讲述一些毫无实用信息的故事。
相似文章
比较 magic statics、std::call_once 和 std::async 的异常行为
本文比较了 C++ 并发机制中的异常处理行为:magic statics、std::call_once 和 std::async,解释了它们的差异并提供了使用建议。
std::call_once 与 std::async
本文比较了 C++ 中用于延迟执行的 std::call_once 和 std::async,讨论了它们在实现、性能和异常行为方面的差异。
C++提醒:函数局部静态变量仅初始化一次,即使看起来像是多次初始化
本文解释了C++中的函数局部静态变量仅初始化一次,并通过一个示例展示如何通过将变量设为静态来避免释放后使用漏洞,同时讨论了共享状态的影响。
静态分配,恒定工作
本文探讨了静态分配策略,以防止释放后使用、类型混淆等内存安全问题,讨论了对象池和代际索引,并介绍了来自TigerStyle的技巧,以在初始化后避免动态内存分配。
C++ 中的惰性初始化
本文解释 C++ 中的惰性初始化,通过将计算推迟到必要时来优化性能,使用辅助结构体来利用 std::optional 中的转换运算符。