본문으로 건너뛰기
Previous
Next
C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기

C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기

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())를 써야 했습니다. beginend를 매번 쌍으로 넘기는 게 번거롭고, 실수로 순서를 바꾸면 (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)처럼 반복자 계산이 필요합니다. 범위 하나만 넘기면 되고, 다음 글의 viewsubrangedrop/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·파이프라인을 이해할 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

1. Range란

개념

rangestd::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_assertv가 range인지 컴파일 타임에 검사할 수 있습니다. std::vectorbegin/ 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_bound4 초과인 첫 위치를 반환합니다.

#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란?

viewstd::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설명예시
rangebegin/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::listbidirectional_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::sortstd::sort내부 구현이 동일하므로 성능 차이가 거의 없습니다. Ranges 버전은 범위를 넘기면 내부적으로 begin/end를 호출해 기존 알고리즘을 호출합니다.

작업std::sortstd::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++20 Ranges, std::ranges, range 개념, 반복자 개선, ranges 알고리즘, 프로젝션, input_range, ranges::sort 등으로 검색하시면 이 글이 도움이 됩니다.

정리

항목내용
rangebegin/end로 순회 가능한 타입
std::ranges::범위를 받는 알고리즘·유틸
view원본을 복사하지 않고 참조만 하는 range
adaptorfilter, transform, take, drop 등 파이프라인 구성 요소
프로젝션비교/연산 전에 적용할 변환 (마지막 인자)
비교자정렬/검색 시 사용할 비교 함수 (두 번째 인자)
conceptsinput_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 libraryRanges TS를 참고하세요. 한 줄 요약: std::ranges로 범위 하나만 넘기면 반복자 쌍 실수가 줄고 가독성이 좋아집니다. 프로젝션으로 멤버 기준 정렬을 한 줄로 쓸 수 있습니다. 다음으로 Views·파이프라인(#25-2)를 읽어보면 좋습니다. 다음 글: [C++ 실전 가이드 #25-2] Ranges Views와 파이프라인: 지연 연산으로 효율적으로 다루기 이전 글: [C++ 실전 가이드 #24-2] 기존 프로젝트를 Module로 전환: 단계별 마이그레이션

관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.