魔法静态变量 vs. std::call_once

The Old New Thing (Raymond Chen) 新闻

摘要

本文比较了两种 C++ 技术:魔法静态变量和 std::call_once,用于缓存计算,并解释了根据作用域和初始化等因素何时使用哪种技术。

<p>假设你有如下函数</p> <pre>bool should_use_widgets() { bool supported = ⟦ complex code to check OS features ⟧; return supported &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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() &amp;&amp; is_configuration_enabled("widgets"); } bool are_widget_carriers_supported() { return are_widgets_supported_in_os() &amp;&amp; is_configuration_enabled("widget_carriers"); } </pre> <p>这种技巧常用于单例模式。</p> <pre>class Singleton { public: static Singleton&amp; GetInstance() { static Singleton instance; return instance; } ⟦ various methods go here ⟧; private: Singleton() = default; Singleton(Singleton const&amp;) = delete; Singleton&amp; operator=(Singleton const&amp;) = 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&lt;Widget&gt; const&amp; widget) : widget(widget) {} bool can_reverse_polarity() { return can_reverse_polarity_cached; } private: std::shared_ptr&lt;Widget&gt; const widget; bool can_reverse_polarity_cached = is_configuration_enabled("polarity_reversal") &amp;&amp; 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") &amp;&amp;</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&lt;Widget&gt; const&amp; 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") &amp;&amp;</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&lt;Widget&gt; 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&lt;T&gt;</code> 类型中。¹</p> <pre>template&lt;typename T, typename L&gt; struct lazy { lazy(L&amp;&amp; l) : init(std::forward&lt;L&gt;(l)) {} T&amp; get() { std::call_once(once, [&amp;] { value.emplace(init()); }); return *value; } private: std::optional&lt;T&gt; value; std::once_flag once; std::decay_t&lt;L&gt; init; }; template&lt;typename T, typename L&gt; lazy&lt;T, L&gt; make_lazy(L&amp;&amp; l) { return { std::forward&lt;L&gt;(l) }; } void test() { auto v = make_lazy&lt;int&gt;([] { 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. &lt;CODE&gt;std::call_once&lt;/CODE&gt;</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 账号上,讲述一些毫无实用信息的故事。

相似文章

std::call_once 与 std::async

The Old New Thing (Raymond Chen)

本文比较了 C++ 中用于延迟执行的 std::call_once 和 std::async,讨论了它们在实现、性能和异常行为方面的差异。

静态分配,恒定工作

Lobsters Hottest

本文探讨了静态分配策略,以防止释放后使用、类型混淆等内存安全问题,讨论了对象池和代际索引,并介绍了来自TigerStyle的技巧,以在初始化后避免动态内存分配。

C++ 中的惰性初始化

Lobsters Hottest

本文解释 C++ 中的惰性初始化,通过将计算推迟到必要时来优化性能,使用辅助结构体来利用 std::optional 中的转换运算符。