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_once or 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 clean lazy<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.