C++ 中的惰性初始化

Lobsters Hottest 工具

摘要

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

<p><a href="https://lobste.rs/s/mdvpm4/being_lazy_c">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/09/13 14:50

# C++中的惰性初始化 - CPP Rendering - Antoine MORRIER 来源:https://cpp-rendering.io/being-lazy-in-c/ ## 背景 我又好久没更新了。看来我得在每篇新文章开头都加上这句话,哈哈。可以说我有点懒——而这恰恰好,因为我们将要讨论如何在C++中实现“懒惰”。 惰性初始化是指将初始化延迟到实际需要时再进行。它可用于优化性能(这也是下一篇文章的主题),但最近我在为一个客户工作时遇到了一个有趣的问题,由此引出了这个话题。 ```cpp int complex_computation() { std::cout << "Compute" << std::endl; return 2; } int main() { std::optional<int> a = 50; std::optional<int> b; std::cout << a.value_or(complex_computation()) << "\n"; std::cout << b.value_or(complex_computation()) << "\n"; } ``` 为什么这段代码的输出不是: ``` 50 Compute 2 ``` 而是: ``` Compute 50 Compute 2 ``` C++的核心信条不应该是“不会为未使用的特性付出代价”吗?在第一种情况下,计算本不需要执行啊…… 细心的读者可能已经注意到,`value_or` 是一个函数,而函数的每个参数在调用前都必须被求值。在C++23中,`std::optional::or_else(f)` 通过接受一个可调用对象解决了这个特定问题。然而,`value_or` 远非唯一存在此问题的函数,因此有必要提供一个通用解决方案。例如,`std::map::try_emplace` 也存在同样的问题。 ## 分析问题 首先需要理解 `value_or` 的作用。MSVC的STL实现大致如下: ```cpp template <typename T> class optional { template <typename U> constexpr T value_or(U&& value) const& { if (this->has_value()) { return **this; } return static_cast<T>(std::forward<U>(value)); } }; ``` 从 `U` 到 `T` 的转换仅发生在回退分支中,因此计算应在此处触发。换句话说,对象 `T` 必须通过将 `U` 转换为 `T` 来具体化。 现在,让我们创建一个简单的辅助结构: ```cpp template <typename F> struct Lazy { // C++17用户需要推导指南(聚合类的CTAD是C++20特性) template <typename T> operator T() const { return initializer(); } F initializer; }; int main() { std::optional<int> a = 50; std::optional<int> b; std::cout << a.value_or(Lazy{complex_computation}) << "\n"; std::cout << b.value_or(Lazy{complex_computation}) << "\n"; } ``` 现在结果完全符合预期: ``` 50 Compute 2 ``` ## 局限性 1. **无记忆化**: ```cpp int main() { std::optional<int> a = 50; std::optional<int> b; std::optional<int> c; Lazy value{complex_computation}; std::cout << a.value_or(value) << "\n"; std::cout << b.value_or(value) << "\n"; std::cout << c.value_or(value) << "\n"; } ``` 由于 `Lazy` 不会记忆化操作结果,计算会执行两次——`b` 和 `c` 各一次。 2. **无约束**:`if (Lazy{...})` 能编译通过,但可能不会按你预期工作。 ## 总结 开头观察到的行为并非 `std::optional` 的过错:C++ 在调用前会求值函数参数,因此 `value_or(complex_computation())` 无论是否需要都会执行计算。 `value_or` 的巧妙之处在于它仅在回退分支中*转换*其参数。我们的 `Lazy` 辅助结构正是利用了这一点:将计算隐藏在转换运算符背后,使得调用者仅在实际需要具体化值时才执行计算,否则跳过。 我们故意保持辅助结构的极简性,这暴露了两个弱点:每次转换都会重新计算结果,且其无约束的转换运算符会静默转换为任何类型(包括 `bool`)。 在下一篇文章中,我们将构建一个更健壮的惰性类型,它能记忆化结果,并仅转换到其初始化器返回的类型,同时测量其运行时开销。 希望你喜欢这篇文章! ## 参考 MSVC STL: optional::value_or (https://github.com/microsoft/STL/blob/main/stl/inc/optional#L503)

相似文章

C++ 中不使用 std::move 的移动

Hacker News Top

本文解释了 C++23 如何在特定情况下允许隐式移动,减少对 std::move 的需求,并通过返回值优化和拷贝省略提升性能。

在C++中模拟函数的关键字参数

Hacker News Top

本文演示了一种通过使用带有指定初始化器的结构体来模拟Python风格关键字参数的技术,无需宏或模板魔法即可提高代码可读性。

编写高效的C++代码

Hacker News Top

本文讨论了编写高效C++代码的技术,着重于面向数据设计和性能优化,适用于游戏和实时处理等应用。

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

The Old New Thing (Raymond Chen)

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

const_cast:必要之恶

Lobsters Hottest

本文解释了为什么在C++中有时必须使用const_cast,特别是从std::priority_queue中移动对象时,以及如何安全地使用它。