C++20 Ranges & Views — 파이프라인 컴포지션과 lazy evaluation
개요 — Ranges 라이브러리란
Section titled “개요 — Ranges 라이브러리란”C++20 Ranges(<ranges>)는 범위(Range) 기반 알고리즘과 뷰(View) 컴포지션을 위한 라이브러리입니다. 기존 <algorithm>의 반복자 쌍(begin, end) 인터페이스를 대체하며, 파이프 연산자(|)를 이용한 함수형 파이프라인 스타일로 데이터를 변환합니다.
핵심 특징:
- Range:
begin()/end()가 있는 모든 컨테이너·배열·뷰 - View: 지연 평가되는 가벼운 범위 어댑터 (복사 비용 없음)
- 파이프 컴포지션:
|연산자로 여러 View를 체이닝 - 알고리즘 오버로드:
std::ranges::sort등 Range를 직접 받는 알고리즘
1. 기존 방식 vs Ranges 방식 비교
Section titled “1. 기존 방식 vs Ranges 방식 비교”#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하게 처리2. 자주 쓰는 View 어댑터
Section titled “2. 자주 쓰는 View 어댑터”2.1 filter, transform
Section titled “2.1 filter, transform”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 100for (int x : pipeline) std::cout << x << " ";2.2 take, drop, take_while, drop_while
Section titled “2.2 take, drop, take_while, drop_while”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}2.3 reverse, keys, values, elements
Section titled “2.3 reverse, keys, values, elements”// 역순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>; // 키만2.4 iota — 숫자 범위 생성
Section titled “2.4 iota — 숫자 범위 생성”// 0부터 9까지for (int i : std::views::iota(0, 10)) std::cout << i << " ";
// 무한 시퀀스 + takefor (int i : std::views::iota(1) | std::views::take(5)) std::cout << i << " "; // 1 2 3 4 52.5 join, split
Section titled “2.5 join, split”// 중첩 범위 평탄화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";3. View 어댑터 요약 표
Section titled “3. View 어댑터 요약 표”| 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::keys | pair의 first / map 키 |
views::values | pair의 second / map 값 |
views::elements<N> | tuple의 N번째 원소 |
views::iota(start, end) | 정수 시퀀스 생성 |
views::join | 중첩 범위 평탄화 |
views::split(delim) | 구분자로 범위 분리 |
views::zip(r1, r2) | 두 범위를 pair로 묶음 (C++23) |
4. std::ranges 알고리즘
Section titled “4. std::ranges 알고리즘”기존 <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_ofauto 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; });5. Lazy Evaluation 동작 원리
Section titled “5. Lazy Evaluation 동작 원리”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은 처리 안 됨)6. Range를 직접 생성하는 방법
Section titled “6. Range를 직접 생성하는 방법”// 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; // Eve8. View 소유권과 주의사항
Section titled “8. View 소유권과 주의사항”// 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 어댑터를 조합하는 것으로 대부분의 데이터 변환 요구사항을 커버할 수 있습니다.