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

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

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

이 글의 핵심

C++ 커스텀 Range 작성: range 개념을 만족하는 타입 만들기 [#25-3]. 실무에서 겪은 문제·range 요구 사항.

들어가며: “커스텀 컨테이너를 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, 프로덕션 패턴까지 실전 수준으로 다룹니다. 실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

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.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( { 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++ 커스텀 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++ 커스텀 Range 작성 | range 개념을 만족하는 타입 만들기 [#25-3]」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(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++ 커스텀 Range 작성 | range 개념을 만족하는 타입 만들기 [#25-3]」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  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 순서를 권장합니다.