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가 없어 수동 인덱스 접근만 가능 - 네트워크 패킷 스트림:
PacketStream을ranges::find_if로 검색하고 싶은데 range가 아니라 알고리즘 적용 불가 - 슬라이싱:
vector의 일부 구간만ranges::sort에 넘기고 싶은데subrange없이 복사해야 함 - 필터/변환 파이프라인: 도메인 타입에
| filter(...) | transform(...)체이닝을 적용하고 싶은데 adaptor가 없음
원인: SensorBuffer에 begin()/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, 프로덕션 패턴까지 실전 수준으로 다룹니다.
목차
- range 요구 사항
- 완전한 커스텀 Range 구현
- Range Adaptor 구현
- Sentinel 기반 Range 예제
- 흔한 실수와 해결법
- 모범 사례 (Best Practices)
- 성능 비교
- 프로덕션 패턴
- view_interface 활용
- 실전 예제
- 구현 체크리스트
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::sort 나 for (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_iterator면ranges::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 예제
Sentinel은 end()가 반복자와 다른 타입일 수 있게 합니다. “끝”을 표현하는 데 반복자 전체가 필요 없을 때 메모리나 비교 비용을 줄일 수 있습니다.
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 == sentinel이it.is_end()한 번 호출로 끝남 - 무한 range: 끝이 없는 range는 sentinel로 “절대 같지 않음”을 표현 가능
5. 흔한 실수와 해결법
문제 1: iterator 요구 사항 미충족
증상: std::ranges::sort(my_range) 컴파일 에러 — “does not satisfy random_access_iterator”
원인: sort는 random_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 발생.
원인: FilterView가 Pred pred_를 값으로 보관하고, Iterator가 Pred* pred_로 참조. FilterView가 이동되면 pred_ 주소가 바뀌어 반복자의 포인터가 무효화.
해결:
// ✅ View를 복사하지 않거나, 반복자가 predicate를 값으로 보관
// 또는 std::reference_wrapper로 base range만 참조
문제 6: iterator_reference_t vs value_type 혼동
증상: transform view에서 operator*가 T를 반환하는데, iterator_traits의 value_type을 T&로 잘못 정의.
원인: operator*가 프록시나 임시를 반환할 때 reference와 value_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_concept가 iterator_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) 추가 |
vector에 push_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++20 Ranges | begin/end 반복 탈출하고 ranges 알고리즘 쓰기
- C++ Ranges | “함수형 프로그래밍” C++20 가이드
- C++ 커스텀 반복자 완벽 가이드 | Forward·Bidirectional
이 글에서 다루는 키워드 (관련 검색어)
C++ 커스텀 range, range adaptor, iterator, range 구현, sentinel, view_interface 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 내용 |
|---|---|
| range | begin/end (또는 sentinel) 제공 |
| 반복자 | ++, *, ==/!=, iterator_traits 호환 |
| sentinel | end()용 타입, iterator와 ==/!= 가능 |
| Range adaptor | 파이프 | 로 체이닝 |
| view_interface | empty, size 등 파생 가능 |
| Lazy/Infinite | 순회 시 계산, take로 제한 |
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 도메인 특화 컨테이너를 for (auto x : ...)나 ranges:: 알고리즘에 바로 넣고 싶을 때, 또는 기존 range를 변환하는 view/adaptor를 만들 때 사용합니다. 로그 버퍼, 센서 데이터, 네트워크 패킷 스트림 등을 range로 노출하면 코드가 깔끔해집니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference - Ranges library와 Ranges 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]