Skip to content

C++20 Ranges & Views — 파이프라인 컴포지션과 lazy evaluation

C++20 Ranges(<ranges>)는 범위(Range) 기반 알고리즘과 뷰(View) 컴포지션을 위한 라이브러리입니다. 기존 <algorithm>의 반복자 쌍(begin, end) 인터페이스를 대체하며, 파이프 연산자(|)를 이용한 함수형 파이프라인 스타일로 데이터를 변환합니다.

핵심 특징:

  • Range: begin()/end()가 있는 모든 컨테이너·배열·뷰
  • View: 지연 평가되는 가벼운 범위 어댑터 (복사 비용 없음)
  • 파이프 컴포지션: | 연산자로 여러 View를 체이닝
  • 알고리즘 오버로드: std::ranges::sort 등 Range를 직접 받는 알고리즘

#include <ranges>
#include <algorithm>
#include <vector>
#include <iostream>
std::vector<int> numbers = {5, 3, 8, 1, 9, 2, 7, 4, 6};
// 기존 방식 — begin/end 명시, 중간 결과 저장 필요
std::vector<int> even_numbers;
std::copy_if(numbers.begin(), numbers.end(),
std::back_inserter(even_numbers),
[](int n) { return n % 2 == 0; });
std::sort(even_numbers.begin(), even_numbers.end());
// 중간 벡터 할당 발생
// Ranges 방식 — 파이프라인, 지연 평가
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (int v : result)
std::cout << v << " "; // 2→4, 4→16, 6→36, 8→64 순 출력
// 중간 컨테이너 없음 — 순회 시점에 각 원소를 lazy하게 처리

std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 짝수만 선택해 제곱
auto pipeline = v
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
// 4 16 36 64 100
for (int x : pipeline) std::cout << x << " ";
auto first5 = v | std::views::take(5); // {1,2,3,4,5}
auto skip3 = v | std::views::drop(3); // {4,5,6,...10}
auto while_lt5 = v | std::views::take_while([](int n){ return n < 5; }); // {1,2,3,4}
auto from5 = v | std::views::drop_while([](int n){ return n < 5; }); // {5,6,...10}
// 역순
auto rev = v | std::views::reverse; // {10,9,8,...1}
// map의 키/값 분리
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}, {"Carol", 92}};
auto names = scores | std::views::keys; // "Alice", "Bob", "Carol"
auto vals = scores | std::views::values; // 95, 87, 92
// pair/tuple의 N번째 원소
auto first_elements = scores | std::views::elements<0>; // 키만
// 0부터 9까지
for (int i : std::views::iota(0, 10))
std::cout << i << " ";
// 무한 시퀀스 + take
for (int i : std::views::iota(1) | std::views::take(5))
std::cout << i << " "; // 1 2 3 4 5
// 중첩 범위 평탄화
std::vector<std::vector<int>> matrix = {{1,2,3},{4,5,6},{7,8,9}};
auto flat = matrix | std::views::join;
// 1 2 3 4 5 6 7 8 9
// 문자열 분리
std::string csv = "apple,banana,cherry";
auto words = csv | std::views::split(',');
for (auto word : words)
std::cout << std::string_view(word) << "\n";

View 어댑터기능
views::filter(pred)조건 true인 원소만 통과
views::transform(func)각 원소에 함수 적용
views::take(n)앞 n개만
views::drop(n)앞 n개 건너뜀
views::take_while(pred)조건 true인 동안만
views::drop_while(pred)조건 true인 동안 건너뜀
views::reverse역순
views::keyspair의 first / map 키
views::valuespair의 second / map 값
views::elements<N>tuple의 N번째 원소
views::iota(start, end)정수 시퀀스 생성
views::join중첩 범위 평탄화
views::split(delim)구분자로 범위 분리
views::zip(r1, r2)두 범위를 pair로 묶음 (C++23)

기존 <algorithm>의 모든 함수는 std::ranges:: 버전을 제공합니다. Range를 직접 받아 begin/end를 명시할 필요가 없습니다.

std::vector<int> data = {5, 3, 8, 1, 9, 2};
// 기존
std::sort(data.begin(), data.end());
// Ranges — 더 간결
std::ranges::sort(data);
std::ranges::sort(data, std::greater{}); // 내림차순
// find, count, any_of, all_of, none_of
auto it = std::ranges::find(data, 8);
int cnt = std::ranges::count_if(data, [](int n){ return n > 5; });
bool any = std::ranges::any_of(data, [](int n){ return n > 10; });
// copy_if — 출력 반복자 필요
std::vector<int> out;
std::ranges::copy_if(data, std::back_inserter(out),
[](int n){ return n % 2 == 0; });

View는 실제 데이터를 복사하지 않고, 원본 범위에 대한 가벼운 참조를 갖습니다. 원소는 순회(iteration) 시점에 하나씩 요청될 때 계산됩니다.

std::vector<int> source = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// pipeline 생성 — 이 시점에는 아무 계산도 안 됨
auto pipeline = source
| std::views::filter([](int n) {
std::cout << "filter " << n << "\n";
return n % 2 == 0;
})
| std::views::transform([](int n) {
std::cout << "transform " << n << "\n";
return n * 10;
})
| std::views::take(3);
std::cout << "-- 순회 시작 --\n";
for (int x : pipeline) std::cout << "결과: " << x << "\n";
// 출력:
// -- 순회 시작 --
// filter 1 / filter 2 / transform 2 / 결과: 20
// filter 3 / filter 4 / transform 4 / 결과: 40
// filter 5 / filter 6 / transform 6 / 결과: 60
// (take(3)으로 3개 확보 후 중단 — 7,8,9,10은 처리 안 됨)

// 1. iota_view — 정수 시퀀스
for (int i : std::ranges::iota_view{0, 100} | std::views::filter([](int n){ return n % 7 == 0; }))
std::cout << i << " "; // 0 7 14 21 ...
// 2. single_view — 단일 원소 범위
auto one = std::views::single(42);
// 3. empty_view — 빈 범위
auto none = std::views::empty<int>;
// 4. repeat_view (C++23) — 값 반복
// auto repeated = std::views::repeat(0, 5); // {0,0,0,0,0}
// 5. counted — 반복자 + 카운트로 범위 생성
int arr[] = {10, 20, 30, 40, 50};
auto sub = std::views::counted(arr + 1, 3); // {20, 30, 40}

7. 실전 패턴 — 데이터 파이프라인

Section titled “7. 실전 패턴 — 데이터 파이프라인”
struct Employee {
std::string name;
std::string department;
int salary;
};
std::vector<Employee> employees = {
{"Alice", "Engineering", 9000},
{"Bob", "Marketing", 7000},
{"Carol", "Engineering", 8500},
{"Dave", "HR", 6000},
{"Eve", "Engineering", 9500},
};
// Engineering 부서 직원의 이름만 추출, 급여 내림차순
auto eng_names = employees
| std::views::filter([](const Employee& e){
return e.department == "Engineering";
})
| std::views::transform([](const Employee& e){ return e.name; });
for (const auto& name : eng_names)
std::cout << name << "\n"; // Alice, Carol, Eve
// 최고 급여 직원 찾기
auto max_it = std::ranges::max_element(employees, {},
[](const Employee& e){ return e.salary; });
std::cout << "최고 급여: " << max_it->name; // Eve

// View는 원본 범위를 참조 — 원본 수명에 주의
auto MakePipeline()
{
std::vector<int> local = {1, 2, 3, 4, 5};
return local | std::views::filter([](int n){ return n > 2; });
// 경고: local은 소멸됨 — 댕글링 참조 발생 가능
}
// 안전한 패턴: owning_view (C++23) 또는 to(컨테이너로 변환)
auto safe = std::vector<int>{1,2,3,4,5}
| std::views::filter([](int n){ return n > 2; });
// C++23: ranges::to — View를 컨테이너로 변환
// auto vec = safe | std::ranges::to<std::vector>();

비교 항목기존 STL 알고리즘C++20 Ranges
인터페이스반복자 쌍 (begin, end)Range 객체 직접 전달
중간 결과별도 컨테이너 필요불필요 (lazy)
코드 스타일명령형선언형 (파이프라인)
지연 평가없음View는 기본적으로 lazy
무한 시퀀스불가iota + take로 가능

C++20 Ranges는 단순히 편리한 문법이 아니라, 불필요한 중간 컨테이너 할당을 없애고 처리 흐름을 명확하게 선언하는 패러다임 전환입니다. 표준 View 어댑터를 조합하는 것으로 대부분의 데이터 변환 요구사항을 커버할 수 있습니다.