Has a Lazy-Initialization Vocabulary Type (std::lazy) Been Proposed for C++?
Introduction: The Missing Vocabulary Type
Modern C++ has steadily introduced expressive vocabulary types into the Standard Library. We have std::tuple for heterogenous collections, std::optional for nullable values, std::variant for type-safe unions, and std::expected for error handling. However, developers frequently ask: Why isn't there a standardized lazy-initialization vocabulary type like std::lazy<T>?
A lazy<T> type would be constructed uninitialized, deferring the construction of T until the object is accessed for the first time. Let us explore the proposals submitted to the ISO C++ Committee (WG21), the design hurdles that hold such a type back, and how to achieve lazy evaluation in modern C++ today.
Has `std::lazy` Been Proposed to the Committee?
The short answer is yes, but primarily in the context of coroutines and asynchronous task execution, rather than as a general-purpose value wrapper.
1. Coroutine Task Types (P0057 / P1056)
During the standardization of C++20 Coroutines, early proposals (such as Gor Nishanov's P1056 and related papers) included a coroutine task type called std::lazy<T> (or std::task<T>). This represented a coroutine computation that begins execution lazily on first await/request. While std::generator<T> made it into C++23, general lazy asynchronous tasks were deferred to the broader C++26 Sender/Receiver framework (P2300: std::execution).
2. General-Purpose Value Wrappers
Proposals for a basic value wrapper—a type that simply couples an unengaged std::optional<T> with an initializer callable—have been discussed in committee mailing lists and study groups (LEWG), but no universal std::lazy<T> has made it into the Working Draft. The primary obstacle is the lack of consensus on a single design that fits all use cases.
Why Is Standardizing `std::lazy` So Hard?
While a basic lazy type sounds straightforward to write, creating a standard vocabulary type involves critical design trade-offs:
1. Thread Safety vs. Zero Overhead
Should accessing std::lazy<T> be thread-safe?
- If Thread-Safe: It requires synchronization primitives (like
std::call_onceor atomic state checks), introducing runtime overhead even when used in strictly single-threaded contexts. - If Not Thread-Safe: It risks subtle data races if accessed concurrently.
2. Factory Storage and Type Erasure
How does the type store the factory function?
- Template Parameter (
lazy<T, F>): Eliminates memory overhead and enables inlining, but lambdas have unique types, making the type unwieldy as a function return type or class member. - Type Erasure (
std::function<T()>): Provides a cleanlazy<T>interface but introduces dynamic memory allocation and indirect call overhead. - Function Pointer: Fast and uniform, but cannot capture surrounding state.
3. Const-Correctness & Mutability
Triggering initialization when reading a const lazy<T>& requires mutating internal state (using mutable fields and internal synchronization), which complicates value semantics.
How to Implement Lazy Initialization in Modern C++
Depending on your requirements, you can achieve lazy initialization cleanly using existing language and library features.
Approach 1: Magic Statics (C++11 Thread-Safe Singletons)
If you need lazy initialization for static/global data, C++11 guarantees thread-safe initialization of local static variables:
const HeavyResource& get_resource() {
// Initialized exactly once on the first call in a thread-safe manner
static const HeavyResource resource = init_resource();
return resource;
}Approach 2: `std::optional` with `std::call_once`
For member variables requiring thread safety and custom initialization:
#include <optional>
#include <mutex>
class Service {
mutable std::optional<HeavyResource> resource_;
mutable std::once_flag init_flag_;
public:
const HeavyResource& resource() const {
std::call_once(init_flag_, [this]() {
resource_.emplace(/* arguments */);
});
return *resource_;
}
};Approach 3: A Custom Single-Threaded `Lazy` Wrapper
If you prefer an explicit, zero-overhead wrapper without thread-synchronization cost:
#include <functional>
#include <optional>
template <typename T, typename Factory = std::function<T()>>
class Lazy {
mutable std::optional<T> value_;
Factory factory_;
public:
explicit Lazy(Factory factory) : factory_(std::move(factory)) {}
T& get() const {
if (!value_) {
value_ = factory_();
}
return *value_;
}
T& operator*() const { return get(); }
T* operator->() const { return &get(); }
};Summary
While the C++ Standard Library does not currently provide a unified std::lazy<T> value wrapper due to conflicting design goals (thread-safety, factory representation, and overhead), lazy evaluation is well-supported through magic statics, std::optional, coroutine generators (std::generator), and the upcoming std::execution (P2300) framework.