Simplify C++ Map Traversal with std::views::keys

Iterating over associative containers like std::map or std::unordered_map has historically required accessing entire key-value pairs, even when only the keys are needed. With the introduction of the Ranges library in C++20, std::views::keys provides a modern, readable, and efficient way to extract and traverse keys directly. This article explains how std::views::keys simplifies map traversal, compares it to older techniques, and highlights its composability within the ranges ecosystem.

The Problem with Traditional Traversal

In associative containers, elements are stored as std::pair<const Key, Value>. Prior to C++20, accessing only the keys required either referencing pair.first manually or using C++17 structured bindings where the value component was left unused:

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

// Pre-C++17: Verbose pair access
for (const auto& pair : scores) {
    process(pair.first);
}

// C++17: Structured bindings with unused variables
for (const auto& [key, value] : scores) {
    process(key); // 'value' is unused and can trigger compiler warnings
}

While functional, both approaches pollute the loop scope with unnecessary information and fail to express direct intent.

How std::views::keys Solves It

Defined in the <ranges> header, std::views::keys is a range adaptor object. It functions as an alias for std::views::elements<0>, specifically targeting the first element of any tuple-like structure, such as std::pair.

When applied to a map, it produces a view containing only the keys:

#include <iostream>
#include <map>
#include <ranges>
#include <string>

int main() {
    std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 80}, {"Charlie", 90}};

    for (const auto& key : scores | std::views::keys) {
        std::cout << key << '\n';
    }
}

Key Advantages

  1. Clear Expressiveness: The loop variable directly represents the key. There are no unused variables, suppressing potential compiler warnings without needing [[maybe_unused]].
  2. Zero Allocation and Lazy Evaluation: std::views::keys does not create a new container or allocate memory. It operates lazily, projecting each key on demand as iteration occurs.
  3. Pipeline Composability: Because it conforms to the range adaptor interface, it can be chained using the pipe operator (|) with other views and standard algorithms:
// Filter and print only keys starting with 'A'
for (const auto& key : scores | std::views::keys 
                              | std::views::filter([](const std::string& k) { return k.starts_with('A'); })) {
    std::cout << key << '\n';
}

By decoupling the key from the associated value at the iteration interface, std::views::keys transforms map traversal from a verbose workaround into clean, idiomatic modern C++.