C++ 中的惰性初始化
摘要
本文解释 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 的移动
本文解释了 C++23 如何在特定情况下允许隐式移动,减少对 std::move 的需求,并通过返回值优化和拷贝省略提升性能。
在C++中模拟函数的关键字参数
本文演示了一种通过使用带有指定初始化器的结构体来模拟Python风格关键字参数的技术,无需宏或模板魔法即可提高代码可读性。
编写高效的C++代码
本文讨论了编写高效C++代码的技术,着重于面向数据设计和性能优化,适用于游戏和实时处理等应用。
魔法静态变量 vs. std::call_once
本文比较了两种 C++ 技术:魔法静态变量和 std::call_once,用于缓存计算,并解释了根据作用域和初始化等因素何时使用哪种技术。
const_cast:必要之恶
本文解释了为什么在C++中有时必须使用const_cast,特别是从std::priority_queue中移动对象时,以及如何安全地使用它。