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·파이프라인을 이해할 수 있습니다.

목차

  1. Range란
  2. ranges 네임스페이스
  3. Ranges 알고리즘 완전 예제
  4. Views와 Adaptors 기초
  5. 프로젝션과 비교자
  6. Range Concepts
  7. 흔한 실수와 해결법
  8. 모범 사례 (Best Practices)
  9. 성능 비교
  10. 프로덕션 패턴

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++ Range Algorithms | “범위 알고리즘” 가이드
  • C++ 커스텀 Range 작성 | range 개념을 만족하는 타입 만들기 [#25-3]
  • C++ Range Adaptor | “범위 어댑터” 가이드

이 글에서 다루는 키워드 (관련 검색어)

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++ Ranges Views와 파이프라인 | 지연 연산으로 효율적으로 다루기 [#25-2]
  • C++ 커스텀 Range 작성 | range 개념을 만족하는 타입 만들기 [#25-3]
  • C++20 Modules |
  • C++ 기존 프로젝트를 Module로 전환 | 단계별 마이그레이션 [#24-2]
  • C++ constexpr 함수와 변수 | 컴파일 타임에 계산하기 [#26-1]