콘텐츠로 이동

C++20 Ranges & Views

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. 실전 패턴 — 데이터 파이프라인

섹션 제목: “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 어댑터를 조합하는 것으로 대부분의 데이터 변환 요구사항을 커버할 수 있습니다.

#include <ranges>
#include <iterator>
// 홀수 인덱스 원소만 반환하는 커스텀 View
// (실제로는 views::stride(2)로 대체 가능하지만 원리 설명용)
template<std::ranges::view V>
class every_other_view : public std::ranges::view_interface<every_other_view<V>>
{
V base_;
public:
every_other_view() = default;
explicit every_other_view(V base) : base_(std::move(base)) {}
auto begin()
{
auto it = std::ranges::begin(base_);
auto end = std::ranges::end(base_);
// 짝수 인덱스만: 0, 2, 4, ...
return std::counted_iterator(it, std::ranges::distance(it, end));
}
auto end() { return std::default_sentinel; }
};
// 더 실용적인 방법: views::stride (C++23)
// for (auto v : data | std::views::stride(2)) ...

#include <ranges>
#include <vector>
std::vector<int> v = {1, 2, 3, 4, 5, 6};
// zip — 두 범위를 묶어 pair로 반환 (C++23)
std::vector<std::string> names = {"a", "b", "c"};
for (auto [num, name] : std::views::zip(v, names))
std::cout << num << ":" << name << " "; // 1:a 2:b 3:c
// stride — N 간격으로 원소 선택 (C++23)
for (auto x : v | std::views::stride(2))
std::cout << x << " "; // 1 3 5
// chunk — N개씩 묶음 (C++23)
for (auto chunk : v | std::views::chunk(2))
{
for (auto x : chunk) std::cout << x << " ";
std::cout << "| ";
}
// 1 2 | 3 4 | 5 6 |
// slide — 슬라이딩 윈도우 (C++23)
for (auto window : v | std::views::slide(3))
{
for (auto x : window) std::cout << x << " ";
std::cout << "| ";
}
// 1 2 3 | 2 3 4 | 3 4 5 | 4 5 6 |
// enumerate — 인덱스 + 값 (C++23)
for (auto [i, x] : v | std::views::enumerate)
std::cout << i << ":" << x << " "; // 0:1 1:2 2:3 ...
// to — View를 컨테이너로 변환 (C++23)
auto evens = v
| std::views::filter([](int n){ return n % 2 == 0; })
| std::ranges::to<std::vector>(); // vector<int>{2, 4, 6}

12. std::ranges 알고리즘 — Projections

섹션 제목: “12. std::ranges 알고리즘 — Projections”

std::ranges 알고리즘은 projection 파라미터를 지원하여 멤버 포인터나 람다로 비교 기준을 설정할 수 있습니다.

#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
struct Person {
std::string name;
int age;
};
std::vector<Person> people = {
{"Charlie", 30}, {"Alice", 25}, {"Bob", 35}
};
// projection으로 name 기준 정렬 (멤버 포인터)
std::ranges::sort(people, {}, &Person::name);
// Alice, Bob, Charlie
// projection으로 age 기준 정렬 (람다)
std::ranges::sort(people, {}, [](const Person& p){ return p.age; });
// Alice(25), Charlie(30), Bob(35)
// projection + 비교자 조합
std::ranges::sort(people, std::greater{}, &Person::age);
// Bob(35), Charlie(30), Alice(25)
// min_element with projection
auto youngest = std::ranges::min_element(people, {}, &Person::age);
std::cout << youngest->name; // Alice
// find_if with projection
auto found = std::ranges::find(people, "Bob", &Person::name);

// 팁 1: 파이프라인 끝에서 한 번만 소비 (재사용 시 재구성 비용)
auto pipeline = data | views::filter(...) | views::transform(...);
// pipeline을 두 번 순회하면 각각 lazy 평가됨 — 의도적인 경우 OK
// 팁 2: 결과를 여러 번 쓴다면 to<vector>로 구체화
auto result = data
| views::filter([](int n){ return n > 0; })
| ranges::to<std::vector>(); // C++23, 구체화 후 재사용
// 팁 3: views::transform + 무거운 연산 → 순회 순서 주의
// filter 먼저 → transform 나중이 더 효율적
auto good = data
| views::filter(is_valid) // 먼저 걸러냄
| views::transform(expensive_op); // 유효한 것만 변환
// 팁 4: views::take는 조기 종료를 보장
// take(n)이 있으면 n개 얻는 순간 나머지 원소는 처리 안 됨
auto first5 = infinite_range() | views::take(5);

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

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