C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기
이 글의 핵심
STL 알고리즘의 불편함을 해결하는 C++20 Ranges. ranges::sort, find, transform, view·adaptor 기초, 프로젝션·비교자, range concepts, 흔한 실수, 모범 사례, 프로덕션 패턴까지.
들어가며: “STL 알고리즘이 불편해요”
문제 시나리오 1: begin/end 반복과 순서 실수
벡터를 정렬하려면 std::sort(v.begin(), v.end())를 써야 했습니다. begin과 end를 매번 쌍으로 넘기는 게 번거롭고, 실수로 순서를 바꾸면 (sort(v.end(), v.begin())) undefined behavior가 됩니다. Person을 나이 기준으로 정렬하려면 비교자 람다를 길게 써야 했고, find 후 erase 같은 패턴은 반복자 관리가 복잡했습니다.
문제 시나리오 2: 여러 컨테이너에 동일한 알고리즘 적용
시나리오: std::vector, std::deque, std::array 등 서로 다른 컨테이너에 같은 정렬·검색 로직을 적용할 때, 각각 begin/end를 호출하는 보일러플레이트가 반복됩니다. 템플릿 함수를 만들어도 매번 반복자 쌍을 넘기는 인터페이스가 번거롭습니다.
문제 시나리오 3: 멤버 기준 정렬·필터링의 람다 지옥
시나리오: Person 구조체를 나이·이름·부서로 정렬하거나, Order 구조체에서 금액이 10000 이상인 항목만 찾을 때, 매번 { return a.member < b.member; } 형태의 람다를 작성해야 합니다. 프로젝션이 없으면 이런 패턴이 코드 전체에 반복됩니다.
문제 시나리오 4: 부분 범위 처리의 복잡성
시나리오: 벡터의 앞 100개만 정렬하거나, [begin+5, end-10) 구간만 검색할 때, std::sort(v.begin(), v.begin()+100)처럼 반복자 계산이 필요합니다. 범위 하나만 넘기면 되고, 다음 글의 view로 subrange나 drop/take를 쓰면 더 깔끔해집니다.
// ❌ 기존 STL 방식의 불편함
std::vector<int> v = {3, 1, 4, 1, 5};
// 1. begin/end 쌍을 매번 넘겨야 함
std::sort(v.begin(), v.end());
// 2. find 결과 확인 후 erase — 반복자 관리 번거로움
auto it = std::find(v.begin(), v.end(), 4);
if (it != v.end())
v.erase(it);
// 3. Person 나이 기준 정렬 — 비교자 람다가 길어짐
struct Person { std::string name; int age; };
std::vector<Person> people = {{"Alice", 30}, {"Bob", 20}};
std::sort(people.begin(), people.end(),
{ return a.age < b.age; });
C++20 Ranges는 이런 불편함을 해소합니다. 범위 하나만 넘기면 되고, 프로젝션으로 “이 멤버 기준으로”를 한 줄로 쓸 수 있으며, 다음 글의 view와 파이프(|)로 “필터 → 변환 → 정렬”을 지연 평가로 연결할 수 있습니다.
flowchart LR
subgraph before["기존 STL"]
B1[begin, end 쌍] --> B2[매번 넘김]
B2 --> B3[순서 실수 위험]
B2 --> B4[비교자 람다 길어짐]
end
subgraph after["Ranges"]
A1[범위 하나] --> A2["ranges sort v"]
A2 --> A3["프로젝션 &Person age"]
A2 --> A4[가독성·안전성 향상]
end
목표:
- range 개념:
begin(r)/end(r)로 순회 가능한 것 - std::ranges:: 알고리즘 (sort, find, transform 등)
- view·adaptor 기초: filter, transform, take, drop 파이프라인
- 프로젝션·비교자로 멤버 기준 정렬·검색
- Range concepts (input_range, forward_range 등)
- 흔한 실수, 모범 사례, 성능 비교, 프로덕션 패턴
컴파일: Ranges는 C++20 기능이므로 g++ -std=c++20(또는 clang++ -std=c++20)으로 빌드하면 됩니다.
이 글을 읽으면:
- range 기반 알고리즘을 쓸 수 있습니다.
- 기존 컨테이너와 사용자 타입을 range로 쓸 수 있습니다.
- 다음 글의 view·파이프라인을 이해할 수 있습니다.
목차
- Range란
- ranges 네임스페이스
- Ranges 알고리즘 완전 예제
- Views와 Adaptors 기초
- 프로젝션과 비교자
- Range Concepts
- 흔한 실수와 해결법
- 모범 사례 (Best Practices)
- 성능 비교
- 프로덕션 패턴
1. Range란
개념
range는 std::ranges::begin(r) 과 std::ranges::end(r) 로 순회할 수 있는 타입입니다. std::vector, std::list, 배열, 그리고 다음 글에서 다루는 view(데이터를 복사하지 않고 범위로만 보는 객체) 등이 해당합니다.
// 복사해 붙여넣은 뒤: g++ -std=c++20 -o ranges_basic ranges_basic.cpp && ./ranges_basic
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3};
static_assert(std::ranges::range<decltype(v)>);
std::ranges::sort(v);
for (int x : v) std::cout << x << " ";
std::cout << "\n";
return 0;
}
실행 결과: 1 2 3 이 한 줄 출력됩니다.
static_assert로 v가 range인지 컴파일 타임에 검사할 수 있습니다. std::vector는 begin/ end를 가지므로 range이며, 사용자 타입도 begin/ end만 제공하면 range로 쓸 수 있습니다.
begin/end의 통일
std::ranges::begin(r)/std::ranges::end(r)는 sentinel을 허용합니다. (반복자와 다른 타입으로 “끝”을 나타낼 수 있음)ranges::begin/end를 쓰면 “범위” 하나만 넘겨도 됩니다.
std::vector<int> v = {3, 1, 2};
std::ranges::sort(v); // 반복자 쌍 대신 범위
std::ranges::sort(v)는 반복자 쌍 대신 범위 전체를 넘기므로, begin/ end 순서를 잘못 쓰는 실수가 줄고 코드도 짧아집니다.
Ranges 알고리즘 동작 흐름
sequenceDiagram
participant Code as 코드
participant Algo as ranges::sort(v)
participant Range as vector v
Code->>Algo: sort(v) — 범위 전달
Algo->>Range: begin(v), end(v) 호출
Range-->>Algo: 반복자 쌍 반환
Algo->>Algo: 내부적으로 sort(begin, end)
Algo-->>Code: (void, v가 제자리 정렬됨)
2. ranges 네임스페이스
알고리즘 위치
기존 std::sort(begin, end) 처럼 반복자 쌍을 받던 알고리즘에 더해, std::ranges::sort(v) 처럼 범위 전체를 받는 버전이 std::ranges 네임스페이스에 들어 있습니다.
#include <algorithm>
#include <ranges>
#include <vector>
std::vector<int> v = {3, 1, 4, 1, 5};
std::ranges::sort(v);
auto it = std::ranges::find(v, 4);
std::ranges::find(v, 4) 는 v 안에서 4를 찾아 그 위치의 반복자를 반환하므로, it != v.end() 인지 확인한 뒤 사용하면 됩니다. <algorithm> 과 <ranges> 를 함께 include 하고, 범위를 넘기는 쪽을 쓰면 코드가 짧고 실수도 줄어듭니다.
3. Ranges 알고리즘 완전 예제
sort, reverse, count
sort, reverse, count 모두 범위 v 를 넘기면 됩니다. count(v, 1) 은 v 안에서 값 1 이 나오는 횟수를 반환합니다.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
std::ranges::sort(v);
// v == {1, 1, 2, 3, 4, 5, 6, 9}
std::ranges::reverse(v);
// v == {9, 6, 5, 4, 3, 2, 1, 1}
auto n = std::ranges::count(v, 1);
std::cout << "count of 1: " << n << "\n"; // 2
return 0;
}
find, find_if
std::ranges::find 는 찾은 위치의 반복자를 반환합니다. find_if는 조건자를 받습니다.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5};
auto it = std::ranges::find(v, 4);
if (it != v.end()) {
std::cout << "Found at index: " << (it - v.begin()) << "\n";
}
// 3보다 큰 첫 원소
auto it2 = std::ranges::find_if(v, { return x > 3; });
if (it2 != v.end()) {
std::cout << "First > 3: " << *it2 << "\n"; // 4
}
return 0;
}
transform (변환)
std::ranges::transform 은 범위의 각 원소에 함수를 적용해 결과를 다른 범위에 씁니다.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> out;
out.resize(v.size());
std::ranges::transform(v, out.begin(), { return x * 2; });
// out == {2, 4, 6, 8, 10}
for (int x : out) std::cout << x << " ";
std::cout << "\n";
return 0;
}
copy, copy_if
std::ranges::copy 는 범위를 그대로 복사합니다. copy_if는 조건을 만족하는 원소만 복사합니다.
#include <ranges>
#include <vector>
#include <iterator>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> even;
std::ranges::copy_if(v, std::back_inserter(even),
{ return x % 2 == 0; });
// even == {2, 4}
return 0;
}
lower_bound, upper_bound (이진 탐색)
std::ranges::lower_bound(v, 4) 는 정렬된 범위 v 에서 4 이상인 첫 위치를 반환합니다. upper_bound는 4 초과인 첫 위치를 반환합니다.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 4, 4, 5};
std::ranges::sort(v); // 이미 정렬된 경우 생략 가능
auto lo = std::ranges::lower_bound(v, 4); // 첫 4
auto hi = std::ranges::upper_bound(v, 4); // 5
std::cout << "count of 4: " << (hi - lo) << "\n"; // 3
return 0;
}
unique + erase (중복 제거)
std::ranges::unique(v) 는 인접한 중복 원소를 맨 앞으로 모아 두고, “유일한 원소들이 있는 구간”의 끝 반복자를 반환합니다. v.erase(last, v.end()) 로 나머지를 지우면 v 에는 서로 다른 값만 남습니다.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {3, 1, 2, 1, 3, 2};
std::ranges::sort(v); // {1, 1, 2, 2, 3, 3}
auto [first, last] = std::ranges::unique(v);
v.erase(last, v.end()); // v == {1, 2, 3}
for (int x : v) std::cout << x << " ";
std::cout << "\n";
return 0;
}
all_of, any_of, none_of
조건 검사 알고리즘입니다. all_of는 모든 원소가 조건을 만족할 때만 true를 반환합니다.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {2, 4, 6, 8};
bool all_even = std::ranges::all_of(v, { return x % 2 == 0; });
std::cout << "all even: " << all_even << "\n"; // true
bool any_gt5 = std::ranges::any_of(v, { return x > 5; });
std::cout << "any > 5: " << any_gt5 << "\n"; // true
return 0;
}
min_element, max_element
최솟값·최댓값의 반복자를 반환합니다.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5};
auto min_it = std::ranges::min_element(v);
auto max_it = std::ranges::max_element(v);
std::cout << "min: " << *min_it << ", max: " << *max_it << "\n";
return 0;
}
count_if (조건 카운트)
조건을 만족하는 원소의 개수를 반환합니다.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9};
auto n = std::ranges::count_if(v, { return x % 2 == 0; });
std::cout << "even count: " << n << "\n"; // 4
return 0;
}
4. Views와 Adaptors 기초
Ranges 알고리즘은 범위를 직접 수정합니다. 반면 view는 원본을 복사하지 않고 범위로만 “보는” 객체입니다. 파이프라인(|)으로 adaptor를 연결하면 지연 평가로 동작합니다. 자세한 내용은 다음 글에서 다루고, 여기서는 기초만 소개합니다.
view란?
view는 std::ranges::view concept을 만족하는 range입니다. 복사·이동 비용이 O(1) 이고, 원본을 참조만 합니다. 따라서 view를 생성해도 원본 데이터는 변경되지 않습니다.
#include <ranges>
#include <vector>
#include <iostream>
namespace vw = std::ranges::views;
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// filter: 짝수만 필터링 (지연 평가)
auto evens = v | vw::filter( { return x % 2 == 0; });
for (int x : evens)
std::cout << x << " "; // 2 4 6 8
std::cout << "\n";
return 0;
}
자주 쓰는 Adaptors
| Adaptor | 설명 | 예시 |
|---|---|---|
| filter | 조건을 만족하는 원소만 | vw::filter(조건) |
| transform | 각 원소에 함수 적용 | vw::transform(함수) |
| take | 앞 N개만 | vw::take(5) |
| drop | 앞 N개 건너뛰기 | vw::drop(3) |
| reverse | 역순 | vw::reverse |
| take_while | 조건 만족하는 동안 | vw::take_while(조건) |
파이프라인 예제: filter → transform → take
#include <ranges>
#include <vector>
#include <iostream>
namespace vw = std::ranges::views;
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 짝수만 → 제곱 → 앞 3개
auto result = v
| vw::filter( { return x % 2 == 0; })
| vw::transform( { return x * x; })
| vw::take(3);
for (int x : result)
std::cout << x << " "; // 4 16 36
std::cout << "\n";
return 0;
}
실행 결과: 4 16 36 — 2, 4, 6이 제곱되어 4, 16, 36이 되고, 앞 3개만 출력됩니다.
view를 벡터로 구체화
view는 지연 평가이므로, 순회할 때마다 계산됩니다. 한 번만 벡터로 만들어두고 싶다면 std::ranges::to<std::vector>()(C++23) 또는 수동으로 순회해 push_back할 수 있습니다.
#include <ranges>
#include <vector>
#include <iostream>
namespace vw = std::ranges::views;
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
auto squared = v | vw::transform( { return x * x; });
// C++20: 수동으로 벡터에 구체화
std::vector<int> out;
for (int x : squared)
out.push_back(x);
// out == {1, 4, 9, 16, 25}
// C++23: ranges::to 사용
// auto out = v | vw::transform(...) | std::ranges::to<std::vector>();
return 0;
}
5. 프로젝션과 비교자
프로젝션 (Projection)
많은 ranges 알고리즘은 projection을 받습니다. “비교하거나 연산할 때 이 함수를 먼저 적용한 결과로 본다”는 의미입니다. 마지막 인자로 전달합니다.
#include <ranges>
#include <vector>
#include <string>
#include <iostream>
struct Person {
std::string name;
int age;
};
int main() {
std::vector<Person> people = {
{"Alice", 30},
{"Bob", 20},
{"Charlie", 25}
};
// age 기준 정렬 — 멤버 포인터 사용
std::ranges::sort(people, {}, &Person::age);
// people: Bob(20), Charlie(25), Alice(30)
// 람다 프로젝션
std::ranges::sort(people, std::ranges::less{},
{ return p.age; });
for (const auto& p : people)
std::cout << p.name << "(" << p.age << ") ";
std::cout << "\n";
return 0;
}
인자 순서: sort(range, comparator, projection)
- comparator를 기본값(
{})으로 두고 projection만 넘기면 됩니다. std::ranges::less{}는a < b비교를 수행합니다.
비교자 (Comparator)
정렬 기준을 바꾸려면 비교자를 두 번째 인자로 넘깁니다.
#include <ranges>
#include <vector>
#include <string>
struct Person { std::string name; int age; };
int main() {
std::vector<Person> people = {{"Alice", 30}, {"Bob", 20}};
// 내림차순 (나이 큰 순)
std::ranges::sort(people, std::ranges::greater{}, &Person::age);
// 이름 기준 오름차순
std::ranges::sort(people, {},
{ return p.name; });
return 0;
}
find_if + 프로젝션
find_if에도 프로젝션을 쓸 수 있습니다. “age가 25인 첫 Person”을 찾을 때:
auto it = std::ranges::find_if(people, { return age == 25; }, &Person::age);
6. Range Concepts
개념 계층
Range는 반복자 종류에 따라 여러 concept으로 나뉩니다. 알고리즘마다 요구하는 concept이 다릅니다.
flowchart TD
range[range] --> input_range[input_range]
input_range --> forward_range[forward_range]
forward_range --> bidirectional_range[bidirectional_range]
bidirectional_range --> random_access_range[random_access_range]
random_access_range --> contiguous_range[contiguous_range]
| Concept | 설명 | 예시 |
|---|---|---|
| range | begin/end로 순회 가능 | vector, list, array |
| input_range | 한 번만 앞으로 순회 | istream, filter view |
| forward_range | 여러 번 순회 가능 | forward_list |
| bidirectional_range | 역방향 순회 가능 | list |
| random_access_range | 임의 접근 O(1) | vector, array |
| contiguous_range | 메모리 연속 | vector, array |
알고리즘별 요구 사항
- sort, nth_element:
random_access_range - reverse:
bidirectional_range - find, count, transform:
input_range이상 - unique:
forward_range이상
#include <ranges>
#include <vector>
#include <list>
int main() {
std::vector<int> v = {3, 1, 2};
std::list<int> lst = {3, 1, 2};
std::ranges::sort(v); // ✅ vector는 random_access_range
// std::ranges::sort(lst); // ❌ list는 bidirectional_range — sort 불가
std::ranges::find(v, 1); // ✅
std::ranges::find(lst, 1); // ✅ find는 input_range면 충분
return 0;
}
concept으로 제약하기
템플릿에서 “정렬 가능한 범위만 받고 싶다”면 std::ranges::random_access_range를 씁니다.
#include <ranges>
#include <vector>
template <std::ranges::random_access_range R>
void my_sort(R& r) {
std::ranges::sort(r);
}
int main() {
std::vector<int> v = {3, 1, 2};
my_sort(v); // ✅
return 0;
}
7. 흔한 실수와 해결법
문제 1: range가 아닌 타입 넘기기
증상: std::ranges::sort(42) 또는 std::ranges::find(ptr, 4) — 컴파일 에러
원인: 정수, raw 포인터 등은 begin/end가 없어 range가 아닙니다.
해결:
// ❌ range 아님
int x = 42;
int* ptr = arr;
// std::ranges::sort(x); // 에러
// std::ranges::find(ptr, 4); // ptr 하나만 넘김 — find는 (range, value) 또는 (range, value, proj)
// ✅ vector, array, span 등 range 사용
std::vector<int> v = {3, 1, 2};
std::ranges::sort(v);
std::span<int> s(arr, n);
std::ranges::find(s, 4);
문제 2: 프로젝션 인자 순서 혼동
증상: std::ranges::sort(people, &Person::age) — 의도와 다르게 동작하거나 에러
원인: 두 번째 인자는 비교자, 세 번째가 프로젝션입니다. &Person::age를 두 번째에 넘기면 “비교자”로 해석되어 타입 에러가 납니다.
해결:
// ❌ 비교자가 프로젝션 자리에 — Person을 int와 비교하려 해서 에러
// std::ranges::sort(people, &Person::age);
// ✅ 비교자 생략 시 {} 로 두고, 프로젝션을 세 번째에
std::ranges::sort(people, {}, &Person::age);
문제 3: sort에 bidirectional_range 넘기기
증상: std::ranges::sort(lst) — “does not satisfy random_access_iterator”
원인: std::list는 bidirectional_range이지 random_access_range가 아닙니다. sort는 임의 접근이 필요합니다.
해결:
// ❌ list는 sort 불가
std::list<int> lst = {3, 1, 2};
// std::ranges::sort(lst); // 에러
// ✅ list를 vector로 복사 후 정렬
std::vector<int> v(lst.begin(), lst.end());
std::ranges::sort(v);
// 또는 list::sort() 멤버 함수 사용
lst.sort();
문제 4: 반복자 무효화
증상: erase 후 반복자 사용 — undefined behavior
원인: vector::erase는 해당 반복자를 무효화합니다. find로 얻은 반복자로 erase한 뒤, 그 반복자를 다시 쓰면 안 됩니다.
해결:
// ✅ erase가 "다음 유효 반복자"를 반환 — 이를 사용
auto it = std::ranges::find(v, 4);
if (it != v.end()) {
it = v.erase(it); // erase 반환값 사용
}
문제 5: view 수명
증상: view를 반환한 뒤 원본이 파괴되면 dangling
원인: view는 원본을 참조만 합니다. 원본이 먼저 파괴되면 view 순회 시 undefined behavior입니다.
해결:
// ❌ 위험: get_view()가 반환한 view가 vec를 참조하는데, vec는 지역 변수
// auto r = get_view(); // vec가 스택에서 사라진 뒤 r 사용 시 UB
// ✅ view를 즉시 사용하거나, 원본과 수명을 함께 관리
std::vector<int> vec = {1, 2, 3};
auto r = vec | std::views::filter( { return x > 1; });
for (int x : r) { /* vec가 살아 있는 동안만 */ }
문제 6: 빈 범위에서 min_element/max_element
증상: 빈 벡터에 std::ranges::min_element(v) 호출 시 undefined behavior
원인: 빈 범위에서는 “최솟값 위치”가 없습니다. 반복자를 반환하면 v.end()가 되지만, *로 역참조하면 UB입니다.
해결:
// ✅ 반환값이 end인지 확인 후 사용
std::vector<int> v = {};
auto it = std::ranges::min_element(v);
if (it != v.end()) {
std::cout << "min: " << *it << "\n";
} else {
std::cout << "empty range\n";
}
문제 7: transform 출력 범위 크기 부족
증상: std::ranges::transform(v, out.begin(), fn) 호출 시 out이 v보다 작으면 버퍼 오버런
원인: transform은 출력 범위에 직접 씁니다. out.size() < v.size()이면 범위 밖 쓰기가 발생합니다.
해결:
// ✅ 출력 벡터 크기 미리 확보
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int> out;
out.resize(v.size()); // 또는 reserve + back_inserter
std::ranges::transform(v, out.begin(), { return x * 2; });
// ✅ back_inserter 사용 (크기 신경 쓸 필요 없음)
std::vector<int> out2;
std::ranges::transform(v, std::back_inserter(out2), { return x * 2; });
문제 8: unique 전 정렬 누락
증상: std::ranges::unique(v) 호출 후에도 중복이 남아 있음
원인: unique는 인접한 중복만 제거합니다. {1, 2, 1, 3}처럼 떨어져 있는 중복은 제거되지 않습니다.
해결:
// ❌ 잘못된 사용: {1, 2, 1, 3} → {1, 2, 1, 3} (변화 없음)
std::vector<int> v = {1, 2, 1, 3};
std::ranges::unique(v); // 인접 중복만 제거
// ✅ 올바른 사용: 정렬 후 unique
std::ranges::sort(v); // {1, 1, 2, 3}
auto [first, last] = std::ranges::unique(v);
v.erase(last, v.end()); // {1, 2, 3}
8. 모범 사례 (Best Practices)
범위를 넘기고 반복자 쌍은 피하기
권장: std::ranges::sort(v)처럼 범위 전체를 넘깁니다. begin/end 쌍은 순서 실수와 타이핑 부담을 늘립니다.
// ✅ 권장: 범위 하나만
std::ranges::sort(v);
std::ranges::find(v, 42);
// ❌ 비권장: 반복자 쌍 (레거시 호환 시에만)
std::sort(v.begin(), v.end());
프로젝션으로 람다 간소화
멤버 기준 정렬·검색 시 멤버 포인터나 프로젝션을 우선 사용합니다.
// ✅ 권장: 멤버 포인터
std::ranges::sort(people, {}, &Person::age);
// ✅ 권장: 간단한 프로젝션
std::ranges::find_if(people, { return a >= 18; }, &Person::age);
// ❌ 비권장: 긴 람다 (프로젝션으로 대체 가능할 때)
std::ranges::sort(people, { return a.age < b.age; });
view 수명 관리
view는 원본을 참조합니다. 원본이 먼저 파괴되면 dangling reference가 됩니다.
// ✅ 권장: view와 원본을 같은 스코프에서 사용
std::vector<int> v = {1, 2, 3};
auto r = v | std::views::filter( { return x > 1; });
for (int x : r) { /* v가 살아 있는 동안만 */ }
// ❌ 위험: view를 반환하는 함수가 지역 벡터를 참조
// auto get_view() {
// std::vector<int> v = {1, 2, 3};
// return v | std::views::filter(...); // v 파괴 후 dangling!
// }
concept으로 템플릿 제약
“정렬 가능한 범위만 받고 싶다”면 std::ranges::random_access_range로 제약합니다.
// ✅ 권장: 요구 사항을 명시
template <std::ranges::random_access_range R>
void safe_sort(R& r) {
std::ranges::sort(r);
}
// ❌ 비권장: 제약 없으면 list 등에 sort 시도 시 컴파일 에러 메시지가 길어짐
template <typename R>
void unsafe_sort(R& r) {
std::ranges::sort(r); // list 전달 시 에러
}
파이프라인 가독성
긴 파이프라인은 한 줄에 하나의 adaptor씩 끊어서 읽기 쉽게 합니다.
// ✅ 권장: 단계별로 구분
auto result = data
| vw::filter(predicate)
| vw::transform(transform_fn)
| vw::take(100);
// ⚠️ 비권장: 한 줄에 너무 많은 adaptor
auto result = data | vw::filter(predicate) | vw::transform(transform_fn) | vw::take(100);
9. 성능 비교
Ranges vs 기존 알고리즘
동일한 연산을 수행할 때, std::ranges::sort와 std::sort는 내부 구현이 동일하므로 성능 차이가 거의 없습니다. Ranges 버전은 범위를 넘기면 내부적으로 begin/end를 호출해 기존 알고리즘을 호출합니다.
| 작업 | std::sort | std::ranges::sort | 비고 |
|---|---|---|---|
| vector 100만 개 정렬 | 1.0x | ~1.0x | 동일 |
| 반복자 쌍 vs 범위 | - | 범위가 약간의 오버헤드 | 무시 가능 |
프로젝션 오버헤드
프로젝션은 요소마다 한 번 호출됩니다. 단순 멤버 접근(&Person::age)은 인라인되어 거의 비용이 없고, 복잡한 람다는 호출 비용이 있을 수 있습니다.
// ✅ 인라인되기 쉬움
std::ranges::sort(people, {}, &Person::age);
// ⚠️ 람다가 복잡하면 호출 비용
std::ranges::sort(people, {}, {
return expensive_computation(p); // 매 비교마다 호출
});
요약
- ranges 알고리즘은 기존 STL과 동등한 성능을 목표로 설계됨
- 가독성·안전성 향상이 주된 이점
- Views와 파이프라인은 중간 컨테이너 없음으로 메모리 이득
10. 프로덕션 패턴
데이터 변환 파이프라인
“필터 → 변환 → 정렬”을 한 흐름으로 처리할 때, views와 ranges 알고리즘을 조합합니다.
#include <ranges>
#include <vector>
#include <iostream>
namespace vw = std::ranges::views;
int main() {
std::vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 짝수만 필터 → 제곱 → 벡터로 구체화
auto result = v
| vw::filter( { return x % 2 == 0; })
| vw::transform( { return x * x; });
std::vector<int> out;
for (auto x : result)
out.push_back(x);
// out == {4, 16, 36, 64}
// C++23: ranges::to
// auto out = v | vw::filter(...) | vw::transform(...) | std::ranges::to<std::vector>();
return 0;
}
조건부 처리 패턴
find → 있으면 처리, 없으면 기본값 패턴입니다.
#include <ranges>
#include <vector>
#include <optional>
std::optional<int> find_and_get(const std::vector<int>& v, int target) {
auto it = std::ranges::find(v, target);
if (it != v.end())
return *it;
return std::nullopt;
}
정렬 + 이진 탐색
정렬된 범위에서 lower_bound / upper_bound로 구간을 찾는 패턴입니다.
#include <ranges>
#include <vector>
// [lo, hi) 구간의 원소 개수
template <std::ranges::random_access_range R>
std::ranges::range_difference_t<R> count_in_range(R& r,
std::ranges::range_value_t<R> lo,
std::ranges::range_value_t<R> hi)
{
std::ranges::sort(r);
auto first = std::ranges::lower_bound(r, lo);
auto last = std::ranges::upper_bound(r, hi);
return last - first;
}
멤버 기준 min/max
프로젝션으로 “나이가 가장 많은 Person”을 찾습니다.
#include <ranges>
#include <vector>
struct Person { std::string name; int age; };
Person& oldest(std::vector<Person>& people) {
auto it = std::ranges::max_element(people, {}, &Person::age);
return *it;
}
로그 필터링 파이프라인
에러 레벨 로그만 추출해 타임스탬프 기준 정렬 후 상위 N개 처리하는 패턴입니다.
#include <ranges>
#include <vector>
#include <algorithm>
struct LogEntry {
int level; // 0=debug, 1=info, 2=warning, 3=error
std::string message;
int timestamp;
};
std::vector<LogEntry> get_recent_errors(
std::vector<LogEntry>& logs, int count)
{
namespace vw = std::ranges::views;
auto errors = logs
| vw::filter( { return e.level >= 3; });
std::vector<LogEntry> result;
for (auto& e : errors)
result.push_back(e);
std::ranges::sort(result, {}, &LogEntry::timestamp);
if (result.size() > static_cast<size_t>(count))
result.resize(count);
return result;
}
다중 조건 정렬 (복합 키)
나이 오름차순, 같은 나이면 이름 오름차순으로 정렬하는 패턴입니다.
#include <ranges>
#include <vector>
#include <tuple>
struct Person { std::string name; int age; };
void sort_by_age_then_name(std::vector<Person>& people) {
std::ranges::sort(people,
{
return std::tie(a.age, a.name) < std::tie(b.age, b.name);
});
}
부분 범위 알고리즘 (subrange)
벡터의 일부 구간만 정렬·검색할 때 std::ranges::subrange를 사용합니다.
#include <ranges>
#include <vector>
int main() {
std::vector<int> v = {9, 8, 7, 6, 5, 4, 3, 2, 1};
// 인덱스 2~6 구간만 정렬
auto sub = std::ranges::subrange(v.begin() + 2, v.begin() + 7);
std::ranges::sort(sub);
// v == {9, 8, 3, 4, 5, 6, 7, 2, 1}
return 0;
}
erase-remove 관용구 (Ranges 스타일)
특정 값을 가진 원소를 모두 제거하는 패턴입니다.
#include <ranges>
#include <vector>
void remove_value(std::vector<int>& v, int value) {
auto [first, last] = std::ranges::remove(v, value);
v.erase(first, last);
}
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ Range Algorithms | “범위 알고리즘” 가이드
- C++ 커스텀 Range 작성 | range 개념을 만족하는 타입 만들기 [#25-3]
- C++ Range Adaptor | “범위 어댑터” 가이드
이 글에서 다루는 키워드 (관련 검색어)
C++20 Ranges, std::ranges, range 개념, 반복자 개선, ranges 알고리즘, 프로젝션, input_range, ranges::sort 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 내용 |
|---|---|
| range | begin/end로 순회 가능한 타입 |
| std::ranges:: | 범위를 받는 알고리즘·유틸 |
| view | 원본을 복사하지 않고 참조만 하는 range |
| adaptor | filter, transform, take, drop 등 파이프라인 구성 요소 |
| 프로젝션 | 비교/연산 전에 적용할 변환 (마지막 인자) |
| 비교자 | 정렬/검색 시 사용할 비교 함수 (두 번째 인자) |
| concepts | input_range, forward_range, random_access_range 등 |
| 반환 | 반복자 또는 서브레인지 |
Ranges 적용 체크리스트
-
std::sort(v.begin(), v.end())→std::ranges::sort(v)로 변경 - 멤버 기준 정렬 시 프로젝션
&T::member사용 - find/min_element 등 반환값이 end인지 확인 후 역참조
- view 사용 시 원본 수명이 view보다 길게 유지되는지 확인
- transform 출력 범위 크기 확보 또는 back_inserter 사용
- unique 사용 전 정렬 여부 확인
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 벡터·리스트 등 컨테이너를 정렬·검색·변환할 때, 반복자 쌍 대신 범위 하나만 넘기고 싶을 때, 프로젝션으로 멤버 기준 정렬·필터링할 때 사용합니다. 데이터 파이프라인 구축 시 views와 함께 쓰면 효율적입니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference - Ranges library와 Ranges TS를 참고하세요.
한 줄 요약: std::ranges로 범위 하나만 넘기면 반복자 쌍 실수가 줄고 가독성이 좋아집니다. 프로젝션으로 멤버 기준 정렬을 한 줄로 쓸 수 있습니다. 다음으로 Views·파이프라인(#25-2)를 읽어보면 좋습니다.
다음 글: [C++ 실전 가이드 #25-2] Ranges Views와 파이프라인: 지연 연산으로 효율적으로 다루기
이전 글: [C++ 실전 가이드 #24-2] 기존 프로젝트를 Module로 전환: 단계별 마이그레이션
관련 글
- C++ Ranges Views와 파이프라인 | 지연 연산으로 효율적으로 다루기 [#25-2]
- C++ 커스텀 Range 작성 | range 개념을 만족하는 타입 만들기 [#25-3]
- C++20 Modules |
- C++ 기존 프로젝트를 Module로 전환 | 단계별 마이그레이션 [#24-2]
- C++ constexpr 함수와 변수 | 컴파일 타임에 계산하기 [#26-1]