Overview

6 Lazy evaluation

This chapter introduces lazy evaluation in C++ as a practical way to defer work until results are actually needed and to remember those results once computed. Because C++ lacks built-in laziness, the approach is to model it with templates, lambdas, and thread-safe primitives so expensive computations aren’t performed prematurely or repeatedly. The core idea marries on-demand calculation with memoization, and it works best when computations are pure, ensuring delayed evaluation doesn’t change outcomes due to intervening side effects.

The text develops a reusable lazy_val wrapper that stores a computation, exposes a value-like interface, and caches the first result, first with a mutex and then more efficiently with std::call_once. It then shows laziness as an optimization technique: a lazy variant of quicksort that partitions eagerly but only fully sorts the partitions that will be shown (e.g., top-k), improving work to roughly O(n + k log k); user-interface views that fetch and cache data and images on demand rather than loading everything up front; and pruning recursion by caching results of pure functions. Classic examples include memoizing Fibonacci (first with a vector cache, then with a two-value rolling cache) and framing dynamic programming as systematic memoization for problems like the Levenshtein distance, where a matrix stores intermediate results to avoid exponential recomputation.

To generalize, the chapter introduces memoization utilities: one that wraps ordinary functions in a cache keyed by argument tuples, and a more sophisticated helper that supports recursive functions by injecting a memoized “self” into recursive calls. Finally, it applies laziness to expression construction with expression templates, building a deferred string-concatenation tree that computes total size once and performs a single allocation and copy, eliminating chains of temporaries. The discussion closes with cautions: storing references in deferred expressions can violate expectations if lifetimes or values change, reinforcing that lazy techniques depend on purity, careful cache design, and thread-safe initialization to be both correct and efficient.

When sorting just a part of the collection, you can optimize the quicksort algorithm not to recursively call itself for the partitions that don’t need to be sorted.
c06_01.png
You don’t need to get the data that’s not shown to the user. You can fetch the required information when it’s needed.
c06_02.png
With fib implemented recursively, you calculate the same value more than once. The recursion tree that calculates fib(5) contains three identical subtrees calculating fib(2) and two identical subtrees calculating fib(3). The multiplication of work grows exponentially with n.
c06_03.png
With fib implemented lazily, you can avoid evaluating the same trees over and over. If one branch calculates fib(2), other branches will use the previously calculated value. Now, the number of nodes this tree has grows linearly with n.
c06_04.png
Each invocation of lev(m, n) calculates the minimum distance for all paths whose last operation is addition, for all paths whose last operation is removal, and for all paths whose last operation is replacing a character.
c06_05.png
When you concatenate strings eagerly, you may need to allocate new memory if the previous buffer isn’t large enough to store the concatenated string.
c06_06.png
Instead of generating a string, operator+ generates an object that remembers which strings should be concatenated. When you need to create the resulting string, you’ll know the exact size of the buffer you need to generate.
c06_07.png

Summary

  • Executing code lazily and caching results can significantly improve the speed of your programs.
  • It’s not often easy (and sometimes not even possible) to construct lazy variants of algorithms you use in your programs, but if you manage to do it, you can make your programs much more responsive.
  • A big class of algorithms has exponential complexity that can be optimized to be linear or quadratic just by caching intermediary results.
  • Levenshtein distance has many applications (in sound processing, DNA analysis, and spell checking, for instance) but can also find its place in regular programs. When you need to notify the UI of changes in a data model, it’s useful to be able to minimize the number of operations the UI needs to perform.
  • Although you should write the caching mechanisms for each problem separately, it’s sometimes useful to have functions such as make_memoized to benchmark the speed gain you could achieve by caching function results.
  • Expression templates are a powerful mechanism for delaying computation. They’re often used in libraries that operate on matrices, and other places where you need to optimize expressions before handing them to the compiler.

FAQ

What is lazy evaluation, and why would I use it in C++?Lazy evaluation is the practice of deferring a computation until its value is actually needed, and caching the result once computed. In C++, this helps avoid unnecessary work (e.g., computing a result that might never be used) and duplicate work (recomputing the same pure result multiple times). While C++ doesn’t provide built-in laziness, you can simulate it with function objects, templates, and caching (memoization).
How does the lazy_val class work?The lazy_val template stores a computation (e.g., a lambda) and a cache of its result. It exposes a const-facing interface while mutating internal cache state via mutable members. A typical design holds: the computation object, a cache value, and synchronization. You can provide a factory like make_lazy_val to deduce the computation’s type conveniently. On first access, it computes and caches; on later accesses, it returns the cached result.
Why replace a mutex with std::call_once for lazy initialization?Using a mutex locks on every access, even after initialization. std::call_once with std::once_flag performs the initialization exactly once and incurs minimal overhead on subsequent calls. It’s simpler, communicates intent (“initialize once”), and is typically faster after the first computation.
How can I sort only part of a collection lazily?Base your approach on quicksort partitioning: partition the range, then recursively sort only the partitions that contain items you must show (e.g., the top k). This avoids fully sorting irrelevant parts. The typical complexity becomes O(n + k log k): O(n) for partitioning work and O(k log k) to sort the needed partition.
How does memoization speed up recursive algorithms like Fibonacci?For pure functions, memoization caches previously computed results keyed by their inputs. In the naive recursive Fibonacci, many subproblems repeat; caching eliminates exponential duplication, reducing the number of evaluations dramatically. A simple cache uses a vector (or map); for Fibonacci specifically, you can optimize further by keeping only the last two results that are actually reused.
What are the trade-offs of caching results?Pros: large speedups by avoiding repeated work; predictable performance after warm-up. Cons: additional memory usage; cache invalidation or eviction policies if memory is constrained; thread-safety needs; correctness depends on function purity (cached results must remain valid).
How does dynamic programming relate to laziness, and what’s the Levenshtein example?Dynamic programming breaks a problem into overlapping subproblems and stores their results for reuse—conceptually similar to memoization. For Levenshtein distance lev(m, n), you can cache distances for all prefixes in a matrix to avoid exponential recursion, reducing complexity to polynomial time by reusing sub-results instead of recomputing them.
How can I generalize memoization for arbitrary pure functions?Wrap the function in a memoizer that maps a tuple of arguments to the result (e.g., std::map, Result>). On call, look up the arguments; if absent, compute, store, and return the result; otherwise, return the cached value. This works well for non-recursive pure functions.
How do I memoize recursive functions properly?Make the function accept a “self” callable used for recursive calls, and pass in a memoizing wrapper. A helper (e.g., make_memoized_r) stores a cache and forwards recursive calls through itself, ensuring all recursion is memoized. This avoids the common pitfall where only the top-level call is cached while internal recursion bypasses the cache.
What are expression templates, and how do they enable lazy string concatenation?Expression templates build a structure representing an expression instead of computing it immediately. For strings, operator+ can build a chain holding references or copies of the operands; only when converting to std::string do you allocate a single right-sized buffer and copy once. Benefits: fewer allocations and copies. Pitfalls: storing references ties lifetime to sources, and laziness relies on purity—mutating source strings before evaluation changes the outcome; prefer copying when needed to avoid surprises.

pro $24.99 per month

  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose one free eBook per month to keep
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime

lite $19.99 per month

  • access to all Manning books, including MEAPs!

team

5, 10 or 20 seats+ for your team - learn more


choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Functional Programming in C++ ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Functional Programming in C++ ebook for free
choose your plan

team

monthly
annual
$49.99
$499.99
only $41.67 per month
  • five seats for your team
  • access to all Manning books, MEAPs, liveVideos, liveProjects, and audiobooks!
  • choose another free product every time you renew
  • choose twelve free products per year
  • exclusive 50% discount on all purchases
  • renews monthly, pause or cancel renewal anytime
  • renews annually, pause or cancel renewal anytime
  • Functional Programming in C++ ebook for free