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 요구 사항
  2. 완전한 커스텀 Range 구현
  3. Range Adaptor 구현
  4. Sentinel 기반 Range 예제
  5. 흔한 실수와 해결법
  6. 모범 사례 (Best Practices)
  7. 성능 비교
  8. 프로덕션 패턴
  9. view_interface 활용
  10. 실전 예제
  11. 구현 체크리스트

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( { 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( { 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.0xO(1) 추가
vectorpush_back으로 결과 저장~1.2x2N (원본+결과)
커스텀 FilterView~1.0xO(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( { return x > 0; })
    | std::views::transform( { 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 =  {
    return std::forward<decltype(r)>(r)
        | std::views::filter( { return x > 0; })
        | std::views::transform( { 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++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기
  • C++ Ranges | “함수형 프로그래밍” C++20 가이드
  • C++ 커스텀 반복자 완벽 가이드 | Forward·Bidirectional

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

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 함수와 변수: 컴파일 타임에 계산하기


관련 글

  • C++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기
  • C++ Ranges Views와 파이프라인 | 지연 연산으로 효율적으로 다루기 [#25-2]
  • C++20 Modules |
  • C++ 기존 프로젝트를 Module로 전환 | 단계별 마이그레이션 [#24-2]
  • C++ constexpr 함수와 변수 | 컴파일 타임에 계산하기 [#26-1]