본문으로 건너뛰기 C++ 커스텀 Range 작성 | range 개념을 만족하는 타입 만들기 [#25-3]

C++ 커스텀 Range 작성 | range 개념을 만족하는 타입 만들기 [#25-3]

C++ 커스텀 Range 작성 | range 개념을 만족하는 타입 만들기 [#25-3]

들어가며: “커스텀 컨테이너를 Range로 만들고 싶어요”

문제 시나리오

도메인 특화 컨테이너래퍼를 만들었는데, for (auto x : myContainer)std::ranges::sort()에 그대로 넣을 수 없어 답답했던 경험이 있으신가요? 표준 vector, array는 범위 기반 for와 ranges 알고리즘에 바로 쓸 수 있지만, 우리가 만든 타입은 컴파일 에러가 납니다.

// 우리가 만든 도메인 컨테이너
class SensorBuffer {
    std::vector<double> data_;
public:
    void push(double v) { data_.push_back(v); }
    double* raw_data() { return data_.data(); }
};
int main() {
    SensorBuffer buf;
    buf.push(1.0); buf.push(2.0);
    // ❌ 컴파일 에러: begin/end가 없음
    // for (auto x : buf) { ... }
    // std::ranges::sort(buf);
}

실제 프로덕션에서 겪는 문제들

  • 로그 버퍼: LogBuffer를 순회해 최근 N개 로그만 처리하고 싶은데 begin/end가 없어 수동 인덱스 접근만 가능
  • 네트워크 패킷 스트림: PacketStreamranges::find_if로 검색하고 싶은데 range가 아니라 알고리즘 적용 불가
  • 슬라이싱: vector의 일부 구간만 ranges::sort에 넘기고 싶은데 subrange 없이 복사해야 함
  • 필터/변환 파이프라인: 도메인 타입에 | filter(...) | transform(...) 체이닝을 적용하고 싶은데 adaptor가 없음 원인: SensorBufferbegin()/end()가 없어서 std::ranges::range 개념을 만족하지 못합니다. 해결: begin/end(또는 sentinel—반복자의 끝을 나타내는 타입)만 정의하면 됩니다.
flowchart LR
  subgraph before["Before: range 아님"]
    B1[SensorBuffer] --> B2[begin/end 없음]
    B2 --> B3[for-range ❌]
    B2 --> B4["ranges sort ❌"]
  end
  subgraph after["After: range 만족"]
    A1[CustomRange] --> A2[begin/end 제공]
    A2 --> A3[for-range ✅]
    A2 --> A4["ranges sort ✅"]
  end

목표

  • std::ranges::range 를 만족하는 타입 설계

  • 반복자 (또는 sentinel) 제공

  • view_interface 로 view처럼 쓰기 (선택) 이 글을 읽으면

  • 커스텀 컨테이너를 range로 노출할 수 있습니다.

  • 반복자와 sentinel을 최소한으로 구현할 수 있습니다.

  • Range adaptor, sentinel 기반 range, 프로덕션 패턴까지 실전 수준으로 다룹니다.

1. range 요구 사항

최소 조건

range이려면:

  • std::ranges::begin(r) 가 유효 (반복자 반환)
  • std::ranges::end(r) 가 유효 (반복자 또는 sentinel 반환)
  • begin(r)end(r) 로 순회 가능 멤버로 begin() / end() 를 제공하거나, ADL로 begin / end 를 찾을 수 있게 하면 됩니다. std::ranges::begin(r) 은 내부적으로 r.begin() (또는 begin(r))을 호출하며, end(r) 도 마찬가지이므로, 이 두 개만 올바르게 반환하면 std::ranges::range<MyRange> 를 만족합니다. static_assert 로 컴파일 타임에 “우리 타입이 range다”를 검증해 두면, 나중에 ranges::sortfor (auto x : myRange) 에 그대로 쓸 수 있습니다.
class MyRange {
public:
    auto begin() const { /* ... */ }
    auto end() const { /* ... */ }
};
static_assert(std::ranges::range<MyRange>);

2. 완전한 커스텀 Range 구현 (begin/end 반복자)

전체 구조

커스텀 range를 만들려면 반복자 타입range 타입을 함께 정의합니다. 반복자는 operator++, operator*, operator==(또는 !=)를 제공해야 합니다.

flowchart TB
  subgraph range[CustomRange]
    R_begin[begin]
    R_end[end]
  end
  subgraph iter[Iterator]
    I_inc[operator++]
    I_deref[operator*]
    I_eq[operator==]
  end
  R_begin --> I_inc
  R_end --> I_eq

반복자 구현 (input_iterator 만족)

다음은 완전한 커스텀 반복자 예제입니다. std::input_iterator 개념을 만족하려면 iterator_traits가 올바르게 동작해야 합니다.

#include <iterator>
#include <ranges>
// 1. 반복자 타입 정의
template <typename T>
class SliceIterator {
    T* ptr_ = nullptr;
    T* end_ = nullptr;
public:
    using value_type = T;
    using difference_type = std::ptrdiff_t;
    using iterator_category = std::input_iterator_tag;
    SliceIterator() = default;
    SliceIterator(T* p, T* e) : ptr_(p), end_(e) {}
    T& operator*() const { return *ptr_; }
    T* operator->() const { return ptr_; }
    SliceIterator& operator++() {
        ++ptr_;
        return *this;
    }
    SliceIterator operator++(int) {
        auto tmp = *this;
        ++*this;
        return tmp;
    }
    friend bool operator==(const SliceIterator& a, const SliceIterator& b) {
        return a.ptr_ == b.ptr_;
    }
};
// 2. iterator_traits 특수화 (C++20에서는 생략 가능한 경우 많음)
template <typename T>
struct std::iterator_traits<SliceIterator<T>> {
    using value_type = T;
    using difference_type = std::ptrdiff_t;
    using iterator_category = std::input_iterator_tag;
};
// 3. Range 타입
template <typename T>
class SliceRange {
    T* data_ = nullptr;
    std::size_t size_ = 0;
public:
    SliceRange(T* data, std::size_t size) : data_(data), size_(size) {}
    auto begin() const {
        return SliceIterator<T>(data_, data_ + size_);
    }
    auto end() const {
        return SliceIterator<T>(data_ + size_, data_ + size_);
    }
};
// 사용 예
#include <vector>
int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    SliceRange<int> slice(v.data(), 3);
    for (auto x : slice)
        std::cout << x << " ";  // 1 2 3
    static_assert(std::ranges::range<SliceRange<int>>);
}

핵심 포인트

  • operator*, operator++, operator== 필수
  • iterator_category는 알고리즘 선택에 사용 (예: random_access_iteratorranges::sort 가능)
  • begin()end()가 같은 반복자 타입을 반환 (sentinel 사용 시에는 다를 수 있음)

random_access_iterator 완전 구현 (ranges::sort 지원)

ranges::sort를 쓰려면 random_access_range가 필요합니다. 다음은 operator+, operator-, operator[], operator< 등을 구현한 예제입니다.

#include <iterator>
#include <ranges>
template <typename T>
class RandomAccessSliceIterator {
    T* ptr_ = nullptr;
public:
    using value_type = T;
    using difference_type = std::ptrdiff_t;
    using iterator_category = std::random_access_iterator_tag;
    RandomAccessSliceIterator() = default;
    explicit RandomAccessSliceIterator(T* p) : ptr_(p) {}
    T& operator*() const { return *ptr_; }
    T& operator const { return ptr_[n]; }
    RandomAccessSliceIterator& operator++() { ++ptr_; return *this; }
    RandomAccessSliceIterator operator++(int) {
        auto tmp = *this;
        ++ptr_;
        return tmp;
    }
    RandomAccessSliceIterator& operator--() { --ptr_; return *this; }
    RandomAccessSliceIterator operator--(int) {
        auto tmp = *this;
        --ptr_;
        return tmp;
    }
    RandomAccessSliceIterator& operator+=(difference_type n) {
        ptr_ += n;
        return *this;
    }
    RandomAccessSliceIterator& operator-=(difference_type n) {
        ptr_ -= n;
        return *this;
    }
    RandomAccessSliceIterator operator+(difference_type n) const {
        return RandomAccessSliceIterator(ptr_ + n);
    }
    RandomAccessSliceIterator operator-(difference_type n) const {
        return RandomAccessSliceIterator(ptr_ - n);
    }
    friend RandomAccessSliceIterator operator+(difference_type n,
            const RandomAccessSliceIterator& it) {
        return it + n;
    }
    difference_type operator-(const RandomAccessSliceIterator& other) const {
        return ptr_ - other.ptr_;
    }
    friend bool operator==(const RandomAccessSliceIterator& a,
            const RandomAccessSliceIterator& b) { return a.ptr_ == b.ptr_; }
    friend bool operator<(const RandomAccessSliceIterator& a,
            const RandomAccessSliceIterator& b) { return a.ptr_ < b.ptr_; }
    friend bool operator>(const RandomAccessSliceIterator& a,
            const RandomAccessSliceIterator& b) { return b < a; }
    friend bool operator<=(const RandomAccessSliceIterator& a,
            const RandomAccessSliceIterator& b) { return !(b < a); }
    friend bool operator>=(const RandomAccessSliceIterator& a,
            const RandomAccessSliceIterator& b) { return !(a < b); }
};
template <typename T>
class RandomAccessSliceRange {
    T* data_ = nullptr;
    std::size_t size_ = 0;
public:
    RandomAccessSliceRange(T* data, std::size_t size)
        : data_(data), size_(size) {}
    auto begin() const {
        return RandomAccessSliceIterator<T>(data_);
    }
    auto end() const {
        return RandomAccessSliceIterator<T>(data_ + size_);
    }
};
// 사용: ranges::sort 가능
// std::vector<int> v = {5, 2, 4, 1, 3};
// RandomAccessSliceRange<int> r(v.data(), v.size());
// std::ranges::sort(r);  // v가 정렬됨

기존 컨테이너 감싸기 (간단한 래퍼)

데이터를 복사하지 않고 기존 컨테이너의 반복자만 넘기는 패턴입니다.

template <typename C>
class Wrapper {
    C* container = nullptr;
public:
    explicit Wrapper(C& c) : container(&c) {}
    auto begin() const { return container->begin(); }
    auto end() const { return container->end(); }
};
// 사용
std::vector<int> v = {3, 1, 4};
Wrapper w(v);
std::ranges::sort(w);  // v가 정렬됨

3. Range Adaptor 구현

Range adaptor는 기존 range를 받아 변환된 view를 반환합니다. 파이프 | 연산자와 함께 사용할 수 있게 만들려면 operator|를 정의합니다.

Adaptor 구조 (Closure + operator|)

flowchart LR
  subgraph adaptor[Range Adaptor 구조]
    A1[range] --> A2[|]
    A2 --> A3[AdaptorClosure]
    A3 --> A4[View 반환]
  end

필터 Adaptor 예제

조건(predicate)을 만족하는 요소만 순회하는 adaptor입니다.

#include <ranges>
#include <algorithm>
#include <iostream>
template <std::ranges::input_range R, typename Pred>
class FilterView : public std::ranges::view_interface<FilterView<R, Pred>> {
    R base_;
    Pred pred_;
public:
    FilterView(R r, Pred p) : base_(std::move(r)), pred_(std::move(p)) {}
    class Iterator {
        std::ranges::iterator_t<R> iter_;
        std::ranges::sentinel_t<R> end_;
        Pred* pred_ = nullptr;
    public:
        using value_type = std::ranges::range_value_t<R>;
        using iterator_category = std::input_iterator_tag;
        Iterator(std::ranges::iterator_t<R> i, std::ranges::sentinel_t<R> e, Pred* p)
            : iter_(i), end_(e), pred_(p) {
            while (iter_ != end_ && !(*pred_)(*iter_)) ++iter_;
        }
        auto& operator*() const { return *iter_; }
        Iterator& operator++() {
            do ++iter_; while (iter_ != end_ && !(*pred_)(*iter_));
            return *this;
        }
        Iterator operator++(int) {
            auto tmp = *this;
            ++*this;
            return tmp;
        }
        friend bool operator==(const Iterator& a, std::ranges::sentinel_t<R> s) {
            return a.iter_ == s;
        }
    };
    auto begin() const {
        return Iterator(std::ranges::begin(base_), std::ranges::end(base_), &pred_);
    }
    auto end() const {
        return std::ranges::end(base_);
    }
};
// Adaptor 객체 (파이프 | 지원)
template <typename Pred>
struct FilterClosure {
    Pred pred_;
    template <std::ranges::range R>
    auto operator()(R&& r) const {
        return FilterView(std::forward<R>(r), pred_);
    }
};
template <std::ranges::range R, typename Pred>
auto operator|(R&& r, const FilterClosure<Pred>& c) {
    return c(std::forward<R>(r));
}
struct FilterAdaptor {
    template <typename Pred>
    auto operator()(Pred p) const {
        return FilterClosure<Pred>{std::move(p)};
    }
};
inline constexpr FilterAdaptor filter;
// 사용
int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    auto evens = v | filter( [](int x) { return x % 2 == 0; });
    for (auto x : evens)
        std::cout << x << " ";  // 2 4
}

Stride View Adaptor (N개마다 하나씩)

매 N번째 요소만 순회하는 adaptor입니다.

#include <ranges>
#include <iostream>
template <std::ranges::input_range R>
class StrideView : public std::ranges::view_interface<StrideView<R>> {
    R base_;
    std::ranges::range_difference_t<R> stride_;
public:
    StrideView(R r, std::ranges::range_difference_t<R> s)
        : base_(std::move(r)), stride_(s) {}
    class Iterator {
        std::ranges::iterator_t<R> iter_;
        std::ranges::sentinel_t<R> end_;
        std::ranges::range_difference_t<R> stride_;
    public:
        using value_type = std::ranges::range_value_t<R>;
        using iterator_category = std::input_iterator_tag;
        Iterator(std::ranges::iterator_t<R> i, std::ranges::sentinel_t<R> e,
                 std::ranges::range_difference_t<R> s)
            : iter_(i), end_(e), stride_(s) {}
        auto& operator*() const { return *iter_; }
        Iterator& operator++() {
            for (auto n = stride_; n > 0 && iter_ != end_; --n) ++iter_;
            return *this;
        }
        Iterator operator++(int) {
            auto tmp = *this;
            ++*this;
            return tmp;
        }
        friend bool operator==(const Iterator& a, std::ranges::sentinel_t<R> s) {
            return a.iter_ == s;
        }
    };
    auto begin() const {
        return Iterator(std::ranges::begin(base_), std::ranges::end(base_), stride_);
    }
    auto end() const { return std::ranges::end(base_); }
};
template <typename Diff>
struct StrideClosure {
    Diff stride_;
    template <std::ranges::range R>
    auto operator()(R&& r) const {
        return StrideView(std::forward<R>(r), stride_);
    }
};
template <std::ranges::range R, typename Diff>
auto operator|(R&& r, const StrideClosure<Diff>& c) {
    return c(std::forward<R>(r));
}
struct StrideAdaptor {
    template <typename N>
    auto operator()(N n) const {
        return StrideClosure<N>{n};
    }
};
inline constexpr StrideAdaptor stride;
// 사용: 0, 2, 4, 6, 8 출력
// std::vector<int> v = {0,1,2,3,4,5,6,7,8,9};
// for (auto x : v | stride(2)) std::cout << x << " ";

Transform Adaptor (간단 버전)

각 요소를 변환하는 adaptor입니다. 표준 std::views::transform을 그대로 활용하는 것이 가장 간단합니다.

// 표준 views::transform 활용 (권장)
#include <ranges>
#include <iostream>
int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    auto doubled = v | std::views::transform( [](int x) { return x * 2; });
    for (auto x : doubled)
        std::cout << x << " ";  // 2 4 6 8 10
}

4. Sentinel 기반 Range 예제

Sentinelend()가 반복자와 다른 타입일 수 있게 합니다. “끝”을 표현하는 데 반복자 전체가 필요 없을 때 메모리나 비교 비용을 줄일 수 있습니다.

null-terminated 문자열 예제

C 스타일 문자열은 '\0'까지 순회합니다. end()가 반복자면 매번 끝까지 이동해 비교해야 하지만, sentinel을 쓰면 *it == '\0' 한 번으로 끝을 판단할 수 있습니다.

#include <ranges>
#include <algorithm>
class CStringIterator {
    const char* ptr_ = nullptr;
public:
    using value_type = char;
    using difference_type = std::ptrdiff_t;
    using iterator_category = std::input_iterator_tag;
    explicit CStringIterator(const char* p) : ptr_(p) {}
    char operator*() const { return *ptr_; }
    CStringIterator& operator++() { ++ptr_; return *this; }
    CStringIterator operator++(int) {
        auto tmp = *this;
        ++*this;
        return tmp;
    }
    bool is_end() const { return *ptr_ == '\0'; }
};
// Sentinel: 반복자와 다른 타입
struct CStringSentinel {};
bool operator==(const CStringIterator& it, CStringSentinel) {
    return it.is_end();
}
bool operator==(CStringSentinel, const CStringIterator& it) {
    return it.is_end();
}
class CStringRange {
    const char* str_ = nullptr;
public:
    explicit CStringRange(const char* s) : str_(s) {}
    auto begin() const { return CStringIterator(str_); }
    auto end() const { return CStringSentinel{}; }
};
// 사용
int main() {
    CStringRange r("Hello");
    for (char c : r)
        std::cout << c;  // Hello
    // ranges 알고리즘
    auto count = std::ranges::count(r, 'l');  // 2
}

라인 단위 파일 읽기 (Sentinel 활용)

std::istream처럼 EOF를 sentinel로 표현하는 예제입니다.

#include <ranges>
#include <sstream>
#include <string>
#include <iostream>
class LineIterator {
    std::istream* stream_ = nullptr;
    std::string line_;
public:
    using value_type = std::string;
    using difference_type = std::ptrdiff_t;
    using iterator_category = std::input_iterator_tag;
    LineIterator() = default;
    explicit LineIterator(std::istream& s) : stream_(&s) {
        ++*this;  // 첫 줄 읽기
    }
    const std::string& operator*() const { return line_; }
    LineIterator& operator++() {
        if (stream_ && std::getline(*stream_, line_)) { /* OK */ }
        else { stream_ = nullptr; }
        return *this;
    }
    LineIterator operator++(int) {
        auto tmp = *this;
        ++*this;
        return tmp;
    }
    bool is_end() const { return stream_ == nullptr; }
};
struct LineSentinel {};
bool operator==(const LineIterator& it, LineSentinel) {
    return it.is_end();
}
bool operator==(LineSentinel, const LineIterator& it) {
    return it.is_end();
}
class LineRange {
    std::istream* stream_ = nullptr;
public:
    explicit LineRange(std::istream& s) : stream_(&s) {}
    auto begin() const { return LineIterator(*stream_); }
    auto end() const { return LineSentinel{}; }
};
// 사용: for (const auto& line : LineRange(std::cin)) { ... }

Sentinel 장점

  • 메모리: end()가 빈 sentinel 객체만 반환 (반복자보다 가벼움)
  • 비교: it == sentinelit.is_end() 한 번 호출로 끝남
  • 무한 range: 끝이 없는 range는 sentinel로 “절대 같지 않음”을 표현 가능

5. 흔한 실수와 해결법

문제 1: iterator 요구 사항 미충족

증상: std::ranges::sort(my_range) 컴파일 에러 — “does not satisfy random_access_iterator” 원인: sortrandom_access_range가 필요합니다. input_iterator만 제공하면 sort를 쓸 수 없습니다. 해결:

// ❌ input_iterator만 있음 → sort 불가
using iterator_category = std::input_iterator_tag;
// ✅ random_access_iterator 제공
using iterator_category = std::random_access_iterator_tag;
// operator+, operator-, operator[], operator< 등 추가

문제 2: concept 만족 실패

증상: static_assert(std::ranges::range<MyRange>) 실패 원인: begin()/end() 반환 타입이 std::input_or_output_iterator를 만족하지 않거나, end()의 sentinel이 begin() 반복자와 비교 가능하지 않음. 해결:

// ❌ begin/end가 void 또는 잘못된 타입
void begin() const;  // 반환 타입 없음
// ✅ 반복자(또는 sentinel) 반환
auto begin() const { return iterator(...); }
auto end() const { return iterator(...); }  // 또는 sentinel

문제 3: const correctness

증상: const MyRange& r에 대해 r.begin() 호출 시 에러 원인: begin()/end()const 멤버가 아님. 해결:

// ❌ const 객체에서 begin 호출 불가
auto begin() { return ...; }
// ✅ const 멤버로 정의
auto begin() const { return ...; }
auto end() const { return ...; }

문제 4: 반복자 무효화

증상: range 순회 중 컨테이너가 수정되어 undefined behavior 원인: vector::push_back 등으로 재할당 시 기존 반복자 무효화. 해결: 순회 중에는 기반 컨테이너를 수정하지 않기. 또는 span처럼 “수명 관리”를 명확히 문서화.

문제 5: Predicate 포인터/참조 수명

증상: FilterView가 람다를 저장하며, 반복자가 내부에서 pred_ 포인터를 쓰는데, view가 복사되면 dangling pointer 발생. 원인: FilterViewPred pred_를 값으로 보관하며, IteratorPred* pred_로 참조. FilterView가 이동되면 pred_ 주소가 바뀌어 반복자의 포인터가 무효화. 해결:

// ✅ View를 복사하지 않거나, 반복자가 predicate를 값으로 보관
// 또는 std::reference_wrapper로 base range만 참조

문제 6: iterator_reference_t vs value_type 혼동

증상: transform view에서 operator*T를 반환하는데, iterator_traitsvalue_typeT&로 잘못 정의. 원인: operator*가 프록시나 임시를 반환할 때 referencevalue_type을 구분해야 함. 해결:

// ✅ C++20에서는 iterator_traits가 operator* 반환 타입에서 추론
// value_type, reference, pointer를 명시적으로 맞추기
using value_type = std::remove_cvref_t<std::iter_reference_t<Iterator>>;

문제 7: 템플릿 인스턴스화 에러

증상: FilterView<std::vector<int>, SomePred>에서 “incomplete type” 또는 “invalid use of undefined type” 에러. 원인: Pred가 람다일 때 람다는 기본 생성 불가. FilterView 생성자에서 Pred를 저장할 때 이동만 가능한 경우 복사 생성자 호출 시 실패. 해결:

// ✅ Pred를 항상 이동으로 전달
FilterView(R r, Pred p) : base_(std::move(r)), pred_(std::move(p)) {}
// closure에서도 std::move(p) 사용

문제 8: Rvalue range 수명

증상: auto v = get_temporary_vector() | filter(pred);v를 순회하면 크래시. 원인: get_temporary_vector()가 임시를 반환하며, filter가 그 참조를 저장. 임시가 파괴된 후 순회하면 dangling. 해결:

// ✅ 임시 range를 바로 순회
for (auto x : get_temporary_vector() | filter(pred)) { ... }
// 또는 view가 range를 값으로 복사/이동하도록 설계

문제 9: operator== 대칭성

증상: it == sentinel은 되는데 sentinel == it는 컴파일 에러. 원인: operator==를 한쪽만 정의. std::ranges 알고리즘은 양방향 비교를 기대할 수 있음. 해결:

// ✅ friend로 양방향 정의
friend bool operator==(const Iterator& it, Sentinel s) { return it.is_end(); }
friend bool operator==(Sentinel s, const Iterator& it) { return it.is_end(); }

문제 10: default_sentinel_t와의 호환

증상: std::default_sentinel_t와 비교 시 에러. 원인: std::ranges::end(r)default_sentinel을 반환하는 view와 조합할 때, 우리 반복자가 default_sentinel과 비교 가능해야 함. 해결:

// ✅ 필요 시 default_sentinel_t와의 operator== 추가
friend bool operator==(const Iterator& it, std::default_sentinel_t) {
    return it.is_end();
}

6. 모범 사례 (Best Practices)

iterator_concept vs iterator_category

C++20에서는 iterator_conceptiterator_category보다 우선합니다. std::random_access_iterator를 만족하려면 iterator_concept를 정의하는 것이 좋습니다.

template <typename T>
class MyIterator {
public:
    using iterator_concept = std::random_access_iterator_tag;
    using iterator_category = std::random_access_iterator_tag;
    using value_type = T;
    using difference_type = std::ptrdiff_t;
    // ...
};

copyable vs move-only range

View는 기본적으로 복사 가능이어야 std::ranges::view 개념을 만족합니다. std::ranges::view를 만족하려면 복사가 저렴해야 합니다 (참조만 복사).

// ✅ 참조로 기반 range 보관 → 복사 시 참조만 복사
template <typename R>
class MyView {
    R base_;  // R이 reference_wrapper일 수 있음
public:
    MyView(R r) : base_(std::move(r)) {}
    // 복사 생성자: base_가 참조면 복사 저렴
};

noexcept 보장

반복자의 operator++, operator*는 가능하면 noexcept로 선언하면 알고리즘 최적화에 도움이 됩니다.

T& operator*() const noexcept { return *ptr_; }
Iterator& operator++() noexcept { ++ptr_; return *this; }

문서화 규칙

  • 수명 요구사항: range가 기반 컨테이너를 참조하는지, 값으로 보관하는지 명시
  • 반복자 무효화 조건: push_back 등으로 언제 무효화되는지 문서화
  • 예외 안전성: begin()/end()가 예외를 던지는지 명시

7. 성능 비교

벤치마크 시나리오

  • 대상: 1,000,000개 정수

  • 작업: 짝수만 필터링 후 합계 | 방식 | 상대 시간 | 메모리 | |------|----------|--------| | vector + 수동 루프 | 1.0x (기준) | N * sizeof(int) | | views::filter (lazy) | ~1.0x | O(1) 추가 | | vectorpush_back으로 결과 저장 | ~1.2x | 2N (원본+결과) | | 커스텀 FilterView | ~1.0x | O(1) 추가 | 요약

  • Lazy view는 중간 컨테이너를 만들지 않아 메모리 효율적.

  • 파이프라인 v | filter(p) | transform(f)는 한 번의 순회로 처리 가능.

  • 커스텀 range가 표준 views보다 느리지 않게 구현하려면, 반복자에서 불필요한 가상 호출·복사를 피해야 합니다.

최적화 팁

// ✅ 참조로 전달 (복사 방지)
template <std::ranges::input_range R>
class MyView {
    R base_;  // 또는 R* / std::reference_wrapper<R>
public:
    explicit MyView(R&& r) : base_(std::forward<R>(r)) {}
    // ...
};
// ✅ 반복자에 캐싱
// predicate 결과 등 반복 접근 값을 멤버로 캐시

8. 프로덕션 패턴

Lazy Evaluation

View는 순회할 때만 계산합니다. 파이프라인을 만들어도 즉시 순회하지 않으면 비용이 들지 않습니다.

auto pipeline = v
    | std::views::filter( [](int x) { return x > 0; })
    | std::views::transform( [](int x) { return x * 2; });
// 아직 아무 계산도 안 함
for (auto x : pipeline) {  // 여기서 처음 계산
    // ...
}

Infinite Range

끝이 없는 range는 sentinel이 “절대 같지 않음”을 반환하도록 합니다. std::views::iota처럼 사용할 때는 take로 잘라 씁니다.

class InfiniteIota {
    int start_ = 0;
public:
    class Iterator {
        int value_;
    public:
        explicit Iterator(int v) : value_(v) {}
        int operator*() const { return value_; }
        Iterator& operator++() { ++value_; return *this; }
        Iterator operator++(int) {
            auto tmp = *this;
            ++*this;
            return tmp;
        }
        bool operator==(const Iterator&) const = default;
    };
    struct Sentinel {};
    friend bool operator==(const Iterator&, Sentinel) { return false; }  // 절대 끝나지 않음
    friend bool operator==(Sentinel, const Iterator&) { return false; }
    explicit InfiniteIota(int start = 0) : start_(start) {}
    auto begin() const { return Iterator(start_); }
    auto end() const { return Sentinel{}; }
};
// 사용: take로 자르기
// for (auto x : InfiniteIota(0) | std::views::take(10))

Composable Pipeline

여러 adaptor를 조합해 재사용 가능한 파이프라인을 만듭니다.

auto positive_doubled = [](auto&& r) {
    return std::forward<decltype(r)>(r)
        | std::views::filter( [](int x) { return x > 0; })
        | std::views::transform( [](int x) { return x * 2; });
};
std::vector<int> v = {-1, 2, -3, 4};
for (auto x : positive_doubled(v))
    std::cout << x << " ";  // 4 8

Range 연결 (concat)

여러 range를 하나로 이어 붙이는 패턴입니다. C++23에서는 std::ranges::views::concat을 사용할 수 있습니다.

#include <ranges>
#include <vector>
#include <iostream>
// C++23: v1 | std::views::concat(v2) | std::views::concat(v3)
// 수동 구현 시: variant 또는 iterator로 두 range 전환

타입 지우기 (Type Erasure)

서로 다른 range 타입을 하나의 인터페이스로 다루고 싶을 때 std::any 또는 가상 함수로 타입을 지울 수 있습니다. 성능이 중요하면 사용을 자제하며, std::variant나 템플릿으로 처리하는 것이 좋습니다.

9. view_interface 활용

상속으로 view 유틸 받기

std::ranges::view_interface<MyView<R>> 를 상속하면 begin() / end() 만 구현해도 empty(), operator bool() (비어 있지 않으면 true), size() (random_access_range일 때) 등이 자동으로 파생됩니다. data() 도 연속 저장소일 때 제공됩니다.

#include <ranges>
template <typename R>
class MyView : public std::ranges::view_interface<MyView<R>> {
    R base_;
public:
    MyView(R r) : base_(std::move(r)) {}
    auto begin() const { return std::ranges::begin(base_); }
    auto end() const { return std::ranges::end(base_); }
};
// empty(), size() 등 자동 사용 가능

10. 실전 예제

인덱스 Range (IotaView)

class IotaView {
    int start_, count_;
public:
    IotaView(int start, int count) : start_(start), count_(count) {}
    class Iterator {
        int value_;
        int limit_;
    public:
        Iterator(int v, int lim) : value_(v), limit_(lim) {}
        int operator*() const { return value_; }
        Iterator& operator++() { ++value_; return *this; }
        Iterator operator++(int) {
            auto tmp = *this;
            ++*this;
            return tmp;
        }
        friend bool operator==(const Iterator& a, const Iterator& b) {
            return a.value_ == b.value_;
        }
    };
    auto begin() const { return Iterator(start_, start_ + count_); }
    auto end() const { return Iterator(start_ + count_, start_ + count_); }
};
// 0..4
for (auto i : IotaView(0, 5))
    std::cout << i << " ";

슬라이스 (시작 인덱스, 개수)

template <std::ranges::random_access_range R>
class Slice {
    R* r = nullptr;
    std::ranges::range_difference_t<R> start_, count_;
public:
    Slice(R& rng, auto start, auto count)
        : r(&rng), start_(start), count_(count) {}
    auto begin() const {
        return std::ranges::begin(*r) + start_;
    }
    auto end() const {
        return std::ranges::begin(*r) + start_ + count_;
    }
};

SensorBuffer를 Range로 만들기 (도입부 문제 해결)

#include <ranges>
#include <vector>
#include <algorithm>
#include <iostream>
class SensorBuffer {
    std::vector<double> data_;
public:
    void push(double v) { data_.push_back(v); }
    double* raw_data() { return data_.data(); }
    std::size_t size() const { return data_.size(); }
    // ✅ begin/end 추가 → range 만족
    auto begin() { return data_.begin(); }
    auto end() { return data_.end(); }
    auto begin() const { return data_.begin(); }
    auto end() const { return data_.end(); }
};
int main() {
    SensorBuffer buf;
    buf.push(1.0); buf.push(2.0); buf.push(3.0);
    for (auto x : buf)
        std::cout << x << " ";  // 1 2 3
    std::ranges::sort(buf);  // 정렬 가능
}

11. 구현 체크리스트

커스텀 range를 만들 때 다음을 확인하세요.

  • begin() / end() (또는 sentinel) 제공
  • 반복자가 operator*, operator++, operator== 구현
  • iterator_category 또는 iterator_concept 정의
  • const 객체에서도 begin() / end() 호출 가능
  • static_assert(std::ranges::range<MyRange>) 통과
  • 수명 요구사항 문서화 (기반 컨테이너 참조 시)
  • ranges::sort 필요 시 random_access_iterator 구현
  • Adaptor는 operator|와 closure 패턴 적용
  • view_interface 상속 시 begin/end만 구현해도 empty 등 파생

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


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

C++ 커스텀 range, range adaptor, iterator, range 구현, sentinel, view_interface 등으로 검색하시면 이 글이 도움이 됩니다.

정리

항목내용
rangebegin/end (또는 sentinel) 제공
반복자++, *, ==/!=, iterator_traits 호환
sentinelend()용 타입, iterator와 ==/!= 가능
Range adaptor파이프 | 로 체이닝
view_interfaceempty, size 등 파생 가능
Lazy/Infinite순회 시 계산, take로 제한

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 도메인 특화 컨테이너를 for (auto x : ...)ranges:: 알고리즘에 바로 넣고 싶을 때, 또는 기존 range를 변환하는 view/adaptor를 만들 때 사용합니다. 로그 버퍼, 센서 데이터, 네트워크 패킷 스트림 등을 range로 노출하면 코드가 깔끔해집니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference - Ranges libraryRanges TS를 참고하세요. 한 줄 요약: begin/end(또는 sentinel)만 맞추면 우리 타입도 range로 쓸 수 있습니다. Range adaptor와 lazy/infinite 패턴으로 실전 활용도를 높일 수 있습니다. 이전 글: C++ 실전 가이드 #25-2: 뷰와 파이프라인 다음 글: [C++ 실전 가이드 #26-1] constexpr 함수와 변수: 컴파일 타임에 계산하기

관련 글