std::views::values in C++ Associative Ranges

Introduced in C++20, std::views::values is a range adaptor that extracts the second element from pair-like or tuple-like elements across a range, making it particularly useful for associative containers like std::map and std::unordered_map. This article covers the primary role of std::views::values, how it eliminates boilerplate code, its lazy evaluation semantics, and how it enables clean range-based pipelines when working with key-value data.

The Core Role of std::views::values

Associative containers store their elements as pairs—typically std::pair<const Key, Value>. Prior to modern range adaptors, iterating exclusively over the values stored in a map required explicitly unpacking the pair using structured bindings or accessing the .second member manually:

std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 88}, {"Charlie", 92}};

// Traditional approach
for (const auto& [name, score] : scores) {
    // 'name' is declared even if unused
    std::cout << score << '\n';
}

The role of std::views::values is to project a range of key-value pairs into a view containing only the values. It applies std::get<1> conceptually to each element in the input range, discarding the key from downstream operations.

Clean Integration with Range Pipelines

Because std::views::values returns a range adaptor closure, it can be composed using the pipe (|) operator. This allows you to chain it with other adaptors, such as std::views::filter or std::views::transform, without needing to account for the key component in subsequent operations.

#include <iostream>
#include <map>
#include <ranges>
#include <numeric>

int main() {
    std::map<std::string, int> inventory = {
        {"Apples", 50},
        {"Oranges", 0},
        {"Bananas", 25},
        {"Grapes", 0}
    };

    // Filter and process only the quantities (values)
    auto in_stock = inventory 
                  | std::views::values 
                  | std::views::filter([](int count) { return count > 0; });

    for (int count : in_stock) {
        std::cout << count << '\n'; // Outputs: 50, 25
    }
}

Reference Preservation and Lazy Evaluation

std::views::values operates lazily. It does not copy or allocate an intermediate container for the values. Instead, it generates values on demand as the range is traversed.

Additionally, std::views::values preserves the reference qualifiers of the underlying values:

  • When operating over a non-const map, the view yields non-const references (Value&), allowing in-place modification of map values.
  • When operating over a const map, it yields const Value&.

Unlike the keys in associative containers—which are marked const to protect container invariants—the mapped values are fully mutable through the view:

std::map<std::string, int> data = {{"A", 1}, {"B", 2}};

// Mutating values directly through the view
for (int& val : data | std::views::values) {
    val *= 10;
}
// 'data' now contains {"A", 10} and {"B", 20}

Summary of Benefits

  • Intent Clarity: Signals immediately that the algorithm or loop operates solely on the payload data, ignoring keys.
  • Composability: Integrates directly with standard range pipelines and constrained algorithms like std::ranges::copy or std::ranges::fold_left.
  • Zero Overhead: Acts as a lightweight wrapper that iterates over the source container without allocations or unnecessary data copying.