---
title: "views::flat_map"
document: P3211R2
date: 2026-06-15
audience: LEWG, SG9 (Ranges)
reply-to:
  - "Hewill Kang <hewillk@gmail.com>"
---

- Abstract
  - Revision history
  - Discussion
  - Design
  - Implementation experience
  - Proposed change
  - References

## Abstract

We propose `views::flat_map`, a range adaptor that applies a function returning a range for each element, then flattens the result. This pattern, commonly known as *flat mapping*, is widespread in functional programming and data processing. Providing it as a dedicated view improves readability and expressiveness, and also opens opportunities for optimization in lazy evaluation contexts.

Noted that this is ranked as Tier 1 in [P2760](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html).

## Revision history

### R0

Initial revision.

### R1

Rename `views::transform_join` to `views::flat_map`.

Introduce new `flat_map_view` class.

Discuss related optimization.

### R2

Support `borrowed_range` in certain cases based on SG9's feedback in Brno.

## Discussion

Mapping each element to a subrange and flattening the results into a single range is common in programming tasks like data processing, string handling, and range composition.

This appears frequently enough in practice to justify direct support in the standard ranges library. Providing `views::flat_map` encourages clearer, more maintainable code and lays the groundwork for potential *optimizations* specific to this use case. While the same behavior can be achieved through composition such as `views::transform` and then `views::join`, a dedicated view enables the library to make stronger semantic guarantees and allows more efficient handling of transform function results, particularly for expensive or lvalue-producing mappers.

This pattern arises naturally in code like:

```
  auto all_courses = students 
                   | std::views::flat_map([](const Student& s) {
                       return std::views::all(s.courses);
                     });
```

## Design

### Why views::flat_map instead of views::transform_join?

The name `transform_join` describes how to combine `transform` and `join`, but it does not show the real concept. Users may see it as two steps, not as a single *map and flatten* operation; The name `flat_map` is common in many languages and clearly shows both mapping and flattening. This makes it easier to find, less confusing, and matches what users expect.

The following table shows how this operation is named in various languages, all of which converge on *flat-map*:

| Language | Function Name | Example | Differences in Semantics |
| --- | --- | --- | --- |
| Haskell | `concatMap` `(>>=)` | `concatMap f [1, 2, 3]` | Purely functional; `>>=` is monadic bind, generalizing flat mapping beyond lists |
| C# (LINQ) | `SelectMany` | `source.SelectMany(x => ...)` | Applies to all IEnumerable; default for flattening nested queries in LINQ syntax |
| Python | `itertools.chain.from_iterable(map(...))` | `list(chain.from_iterable(map(f, data)))` | No built-in `flatMap`; semantics are manual; relies on strict evaluation and eager lists |
| Java | `flatMap` | `stream.flatMap(f)` | Requires the mapper function to return a Stream; lazy evaluation is enforced |
| JavaScript | `flatMap` | `array.flatMap(x => [x, x + 1])` | One-level flattening only; not lazy; limited to arrays |
| Kotlin | `flatMap` | `list.flatMap { listOf(it, it * 2) }` | Similar to Java, but cleaner syntax; strict (not lazy) evaluation for standard collections |
| Rust | `flat_map` | `iter.flat_map(\|x\| some_iter(x))` | Applies to iterators; lazy by default; consumes the original iterator |
| Swift | `flatMap` | `array.flatMap { [$0, $0 * 2] }` | Semantics changed in Swift 4 - used to also remove optionals; now strictly flatten + map |

Given this widespread usage, `flat_map` is the most appropriate and intuitive name for the proposed view. It reflects established terminology, avoids over-specifying implementation details, and aligns well with programmer expectations.

It is worth noting that the `range/v3` uses the name `views::for_each` for this operation. Because such naming deviates further from common terminology and could cause additional confusion, it is not considered a suitable option.

Additionally, C++23 introduced a container named `std::flat_map`, which is a associative container that stores key-value pairs. This is conceptually quite different from the proposed `views::flat_map`, which is a range adaptor that composes a map-then-flatten operation. Clarifying this distinction helps avoid confusion between the two.

Finally, while `flat_map` is preferred for its clarity and alignment with established usage, other naming alternatives are acceptable as long as they convey the correct semantics.

### Why shouldn't be views::transform(f) | views::join?

Although `flat_map` can be expressed as a composition of `transform` and `join`, a dedicated `flat_map_view` provides clearer semantics and more efficient behavior in practice.

The first minor issue with composition is that it interferes with `base()`. When writing `transform(f) | join`, the result holds a `transform_view` internally, so calling `base()` does not give access to the original range, which can be surprising and inconvenient in generic contexts.

Second, a standalone view can manage both the outer iterator and the inner range. It can cache the transform result and avoid *repeated* calls, which is important when the function returns an lvalue range.

In the composed form, `join_view` has no knowledge of how the inner range was produced - it treats each element as if it were obtained by dereferencing the outer iterator. This works fine when the base is something like vector of vectors, where dereferencing is cheap and deterministic. But with `transform | join`, each inner range is the result of invoking a user-defined transformation, which could be expensive to compute even if the result is a stable lvalue. For example:

```
  auto outer   = views::iota(1, 5);
  auto inner   = views::iota(1, 10);
  auto flatten = views::flat_map(outer, [&](int i) -> auto& { return inner; });
  println("{}", flatten);      // calls transform function 40 times
```

While `cache_latest` can mitigate redundant evaluation, it forces the result to model only an `input_range`, regardless of the actual capabilities of the underlying ranges. It also adds an extra layer of adaptor composition and complexity. In contrast, a dedicated `flat_map_view` can handle this caching internally while preserving the strongest valid iterator category.

A dedicated view also avoids unnecessary template instantiations. Since a flat-mapped range can never be more than bidirectional, there is no benefit in preserving random-access capabilities through `transform_view`. However, when using adaptor composition, those capabilities may still be instantiated - even though `join_view` will downgrade them - leading to code bloat and slower compilation.

In short, using composition is flexible but less efficient and can be awkward. `flat_map_view` avoids repeated evaluations, keeps the best iterator type, and makes the view stack simpler.

Although paper [P2760](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html#simple-adaptor-compositions) suggests there is little benefit to providing a separat view as state: "*Importantly, there really isn't much benefit to providing a bespoke `transform_join` as opposed to simply implementing it in terms of these two existing adaptors.*", practical usage has revealed that the composed form introduces subtle inefficiencies, especially in the presence of expensive transformation functions, and not easy to eliminate without internal knowledge of how adaptors interact.

### How to store inner ranges

When the inner range is a reference, we can avoid repeated invocations of the mapping function by storing a pointer to the result instead. This allows us to simply dereference the pointer whenever the inner range is needed, rather than invoking the transformation again — a technique somewhat analogous to `cache_latest_view`. However, it's important to note that this pointer must be stored inside the iterator, not the `flat_map_view` itself, in order to ensure correct behavior in multi-pass scenarios where multiple iterators may coexist and advance independently.

For prvalue inner ranges, the situation is somewhat more tricky, especially for *immovable* ranges.

For `join_view`, it underlying range's iterator already returns the inner range. This property makes it directly suitable for `*non-propagating-cache*::*emplace-deref*`, which caches the inner range by dereferencing the outer iterator.

In contrast, `flat_map_view` obtains the inner range by applying a mapping function to the element referenced by the outer iterator, rather than by directly dereferencing the outer iterator to yield the inner range. As a result, it cannot straightforwardly use `*emplace-deref*` to cache the inner range. Instead, it must store the entire inner range object returned by the mapping function within it to properly extend its lifetime.

One possible approach to mitigate this is to define a proxy iterator that models invoking the mapping function upon dereference. This proxy can then be used with `*emplace-deref*` to cache the mapped inner range indirectly, albeit with additional complexity.

### Support for stashing flattening

At first glance, `flat_map_view` might not have a stashing issue as discussed in [P2770](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2770r0.html), since its inner range is always produced by applying a mapping function, rather than being a subrange stored within the outer range.

However, in particular, the mapping function can certainly return a range whose lifetime is tied to the outer iterator - for example, applying `views::split('x')` to strings obtained from an `istream_iterator<string>` — then `flat_map_view` need cache the current outer iterator to ensures the inner range remains valid during iteration. Therefore, `flat_map_view` follows the design pattern of that paper.

## Implementation experience

The author implemented `views::flat_map` based on libstdc++, see [here](https://godbolt.org/z/zrzd9ox8h).

The implementation supports `input_range`, `forward_range`, and `bidirectional_range`, and demonstrates correct caching behavior for both lvalue and prvalue immovable mapped ranges.

## Proposed change

This wording is relative to [Latest Working Draft](https://eel.is/c++draft).

## References

[P2760R1]

Barry Revzin. A Plan for C++26 Ranges. URL:

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2760r1.html

[P2770R0]

Tim Song. Stashing stashing iterators for proper flattening. URL:

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2770r0.html
