View Adapters vs View Factories in C++ Ranges

The C++20 Ranges library introduced a modern, composable way to work with sequences of data using views—lightweight, non-owning objects that operate lazily. Within this system, views are primarily constructed using two distinct mechanisms: view factories and view adapters. While view factories generate brand-new ranges out of scalar values or state, view adapters take an existing range and transform or filter it into a new range. Understanding the architectural difference between the two is essential for writing efficient, idiomatic range pipelines in modern C++.

What Is a View Factory?

A view factory is a function object that creates a view from scratch, without requiring an existing range as input. It serves as an origin point or data producer in a ranges pipeline. View factories typically accept arguments like values, boundaries, or types to construct a sequence dynamically and lazily on demand.

Common examples of standard view factories include:

  • std::views::iota: Produces an infinite or bounded sequence of sequentially increasing values starting from an initial element (e.g., std::views::iota(1, 10)).
  • std::views::single: Creates a view containing exactly one element provided as an argument.
  • std::views::empty: Produces an empty view of a specified type.
  • std::views::istream: Creates a view that reads elements iteratively from an input stream.
  • std::views::repeat (introduced in C++23): Repeatedly yields the same value indefinitely or a bounded number of times.

Because view factories create ranges out of non-range inputs, they cannot be placed on the right-hand side of a pipe operator (|) to consume an upstream range. Instead, they appear at the very beginning of a pipeline:

auto numbers = std::views::iota(1, 100); // View factory creating numbers from 1 to 99

What Is a View Adapter?

A view adapter is a utility that takes an existing range as input and produces a new view that wraps, transforms, or filters the underlying elements. Unlike factories, adapters always require a source range to operate.

View adapters are designed to be composable. They can be invoked directly as normal functions or chained together using the pipe operator (|).

Common examples of standard view adapters include:

  • std::views::transform: Applies a transformation function to each element in the input range.
  • std::views::filter: Yields only elements from the input range that satisfy a given predicate.
  • std::views::take: Takes the first N elements from a range and ignores the rest.
  • std::views::drop: Skips the first N elements of a range and yields the remaining elements.
  • std::views::reverse: Iterates over an bidirectional range in reverse order.

Because view adapters take a range as their first argument, they can accept inputs from standard containers or from ranges produced by other views:

std::vector<int> data = {1, 2, 3, 4, 5};
auto evens = data | std::views::filter([](int n) { return n % 2 == 0; }); // View adapter

Key Differences Summarized

Characteristic View Factory View Adapter
Input Requirement Values, states, or parameters (non-ranges) An existing range (plus optional arguments)
Pipeline Position Source (the start of a pipeline) Intermediate step or final transformation
Piping Mechanism Cannot consume input via the | operator Can consume input via the | operator
Primary Purpose Generate a sequence of elements Transform, filter, or reorder an existing sequence

In practice, range pipelines frequently combine both concepts: a view factory initializes the data stream, and one or more view adapters reshape that stream into the desired output.