C++ Range Adaptors | Pipeline Composition in C++20

C++ Range Adaptors | Pipeline Composition in C++20

이 글의 핵심

Practical guide to C++ range adaptors: concepts, pipelines, and production patterns with examples.

What are range adaptors?

Function objects that transform a range into a view (C++20).

#include <ranges>

std::vector<int> v = {1, 2, 3, 4, 5};

// Adaptor: range -> view
auto filtered = v | std::views::filter([](int x) { return x > 2; });

Basic usage

namespace vws = std::views;

std::vector<int> v = {1, 2, 3, 4, 5};

// Apply adaptor (functional style)
auto view1 = vws::filter(v, [](int x) { return x > 2; });

// Pipeline
auto view2 = v | vws::filter([](int x) { return x > 2; });

Practical examples

Example 1: Pipeline composition

#include <ranges>
#include <vector>

std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// Several adaptors
auto pipeline = numbers
    | std::views::filter([](int x) { return x % 2 == 0; })  // evens
    | std::views::transform([](int x) { return x * x; })    // squares
    | std::views::take(3);                                   // first 3

for (int x : pipeline) {
    std::cout << x << " ";  // 4 16 36
}

Example 2: Reusable adaptors

#include <ranges>

// Store adaptors
auto evenFilter = std::views::filter([](int x) { return x % 2 == 0; });
auto square = std::views::transform([](int x) { return x * x; });

std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = {6, 7, 8, 9, 10};

// Reuse
auto result1 = v1 | evenFilter | square;
auto result2 = v2 | evenFilter | square;

Example 3: Custom-style adaptors

#include <ranges>

// Odds only
auto odds = std::views::filter([](int x) { return x % 2 != 0; });

// Multiples of 3
auto multiplesOf3 = std::views::filter([](int x) { return x % 3 == 0; });

std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

auto result = v | odds;  // 1 3 5 7 9

Example 4: Conditional adaptor

#include <ranges>

template<typename Range>
auto conditionalFilter(Range&& r, bool applyFilter) {
    if (applyFilter) {
        return std::forward<Range>(r) 
            | std::views::filter([](int x) { return x > 5; });
    } else {
        return std::forward<Range>(r) | std::views::all;
    }
}

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    auto result = conditionalFilter(v, true);
    
    for (int x : result) {
        std::cout << x << " ";  // 6 7 8 9 10
    }
}

Main adaptors

namespace vws = std::views;

// Filtering
vws::filter(pred)
vws::take(n)
vws::drop(n)
vws::take_while(pred)
vws::drop_while(pred)

// Transform
vws::transform(func)
vws::reverse

// Split/join
vws::split(delimiter)
vws::join

// Generation
vws::iota(start)
vws::iota(start, end)

// Other
vws::all  // full range as view
vws::counted(it, n)
vws::common

Common problems

Problem 1: Type inference

// ❌ Verbose type
std::vector<int> v = {1, 2, 3};
std::ranges::filter_view<std::ranges::ref_view<std::vector<int>>, /* ... */> filtered = 
    v | std::views::filter([](int x) { return x > 1; });

// ✅ auto
auto filtered = v | std::views::filter([](int x) { return x > 1; });

Problem 2: Adaptor order

std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// Order changes the result
auto r1 = v | std::views::reverse | std::views::take(3);
// 10 9 8

auto r2 = v | std::views::take(3) | std::views::reverse;
// 3 2 1

Problem 3: Lifetime

// ❌ Temporary destroyed
auto getView() {
    std::vector<int> v = {1, 2, 3};
    return v | std::views::filter([](int x) { return x > 1; });
    // v destroyed
}

// ✅ Clear ownership or reference
auto getView(const std::vector<int>& v) {
    return v | std::views::filter([](int x) { return x > 1; });
}

Problem 4: Performance

// Lazy: recomputed each pass
auto view = v | std::views::filter(pred) | std::views::transform(func);

for (int x : view) { /* ... */ }  // compute
for (int x : view) { /* ... */ }  // compute again

// ✅ Cache when you need one pass worth of work repeatedly
std::vector<int> cached(view.begin(), view.end());
for (int x : cached) { /* ... */ }  // use cache
for (int x : cached) { /* ... */ }  // use cache

Combining adaptors

namespace vws = std::views;

// Pipeline of adaptor objects
auto pipeline = vws::filter(pred) 
              | vws::transform(func) 
              | vws::take(n);

// Apply to data
std::vector<int> v = {1, 2, 3, 4, 5};
auto result = v | pipeline;

FAQ

Q1: What is a range adaptor?

A: It turns a range into a view.

Q2: Pipelines?

A: Combine with |.

Q3: Lazy evaluation?

A: Computed when you iterate.

Q4: Reuse?

A: You can store adaptor objects and reuse them.

Q5: Order?

A: It matters for correctness and efficiency.

Q6: Learning resources?

A:

  • “C++20 Ranges”
  • “C++20 The Complete Guide”
  • cppreference.com

  • C++ range algorithms
  • C++ Ranges library guide
  • C++ Views guide

Practical tips

Tips you can apply at work.

Debugging

  • When something breaks, check compiler warnings first
  • Reproduce with a small test case

Performance

  • Do not optimize without profiling
  • Define measurable targets first

Code review

  • Pre-check areas that often get flagged in review
  • Follow team conventions

Production checklist

Things to verify when applying this idea in practice.

Before coding

  • Is this technique the best fit for the problem?
  • Can teammates understand and maintain it?
  • Does it meet performance requirements?

While coding

  • Are all compiler warnings addressed?
  • Are edge cases considered?
  • Is error handling appropriate?

At review

  • Is intent clear?
  • Are tests sufficient?
  • Is it documented?

Use this checklist to reduce mistakes and improve quality.


Keywords covered

Search for C++, range, adaptor, pipeline, C++20 to find this post.


  • C++20 Ranges basics series
  • C++ range algorithms
  • C++ Ranges basics
  • C++ barrier & latch
  • C++ branch prediction