C++ Custom Ranges | Building Types That Satisfy the Range Concept [#25-3]
이 글의 핵심
How to turn your own containers into C++20 ranges: requirements, iterators, range adaptors, sentinel-based ranges, common mistakes, and patterns drawn from real projects.
Introduction: “I want my custom container to be a range”
Problem scenarios
Have you built a domain-specific container or wrapper and found you cannot use for (auto x : myContainer) or std::ranges::sort() on it? Standard vector and array work with range-based for and ranges algorithms out of the box, but your own type may not compile.
// A domain-specific container we wrote
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);
// ❌ Compile error: no begin/end
// for (auto x : buf) { ....}
// std::ranges::sort(buf);
}
Issues you see in production:
- Log buffer: You want to traverse
LogBufferand process only the last N logs, but withoutbegin/endyou are stuck with manual indexing. - Network packet stream: You want
ranges::find_ifonPacketStream, but it is not a range so algorithms do not apply. - Slicing: You want to pass only part of a
vectortoranges::sortwithout copying everything—withoutsubrangeyou end up copying. - Filter/transform pipelines: You want
| filter(...) | transform(...)on a domain type, but there is no adaptor.
Cause: SensorBuffer has no begin()/end(), so it does not model std::ranges::range. Fix: Provide begin/end (or a sentinel—a type marking the end for an iterator).
flowchart LR
subgraph before["Before: not a range"]
B1[SensorBuffer] --> B2[no begin/end]
B2 --> B3[for-range ❌]
B2 --> B4["ranges sort ❌"]
end
subgraph after["After: satisfies range"]
A1[CustomRange] --> A2[begin/end provided]
A2 --> A3[for-range ✅]
A2 --> A4["ranges sort ✅"]
end
Goals:
- Design a type that satisfies
std::ranges::range - Provide iterators (or sentinels)
- Optionally use
view_interfaceso the type behaves like a view
After reading this post you will:
- Expose custom containers as ranges
- Implement iterators and sentinels with minimal surface area
- Cover range adaptors, sentinel-based ranges, and production patterns
Practical experience: This article is based on real problems and fixes from large C++ codebases. It includes pitfalls and debugging tips that textbooks often skip.
Table of contents
- Range requirements
- Complete custom range implementation
- Range adaptor implementation
- Sentinel-based range examples
- Common mistakes and fixes
- Best practices
- Performance comparison
- Production patterns
- Using view_interface
- Worked examples
- Implementation checklist
1. Range requirements
Minimum conditions
To be a range:
std::ranges::begin(r)is valid (returns an iterator)std::ranges::end(r)is valid (returns an iterator or sentinel)- You can iterate with
begin(r)andend(r)
Provide begin() / end() as members, or make begin / end discoverable via ADL. std::ranges::begin(r) calls r.begin() (or begin(r)) internally; end(r) works the same way. If both return the right types, std::ranges::range<MyRange> holds. A static_assert at compile time documents that your type is a range and keeps ranges::sort and for (auto x : myRange) working.
class MyRange {
public:
auto begin() const { /* ....*/ }
auto end() const { /* ....*/ }
};
static_assert(std::ranges::range<MyRange>);
2. Complete custom range implementation (begin/end iterators)
Overall structure
A custom range pairs an iterator type with a range type. Iterators need operator++, operator*, and operator== (or !=).
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
Iterator implementation (satisfying input_iterator)
Below is a full custom iterator example. To model std::input_iterator, iterator_traits must line up correctly.
#include <iterator>
#include <ranges>
// 1. Iterator type
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 specialization (often unnecessary in 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 type
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_);
}
};
// Usage
#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>>);
}
Key points:
operator*,operator++,operator==are requirediterator_categoryaffects algorithm selection (e.g.random_access_iteratorenablesranges::sort)begin()andend()return the same iterator type (they may differ when using a sentinel)
Full random_access_iterator (supporting ranges::sort)
ranges::sort needs a random_access_range. The following adds operator+, operator-, operator[], operator<, and friends.
#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[](difference_type n) 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_);
}
};
// Usage: ranges::sort works
// std::vector<int> v = {5, 2, 4, 1, 3};
// RandomAccessSliceRange<int> r(v.data(), v.size());
// std::ranges::sort(r); // sorts v
Wrapping an existing container (simple wrapper)
Forward iterators from an existing container without copying data.
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(); }
};
// Usage
std::vector<int> v = {3, 1, 4};
Wrapper w(v);
std::ranges::sort(w); // sorts v
3. Range adaptor implementation
A range adaptor takes an existing range and returns a transformed view. To support the pipe | operator, define operator|.
Adaptor structure (closure + operator|)
flowchart LR
subgraph adaptor[Range adaptor structure]
A1[range] --> A2[|]
A2 --> A3[AdaptorClosure]
A3 --> A4[returns View]
end
Filter adaptor example
An adaptor that traverses only elements matching a predicate.
#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 object (pipe | support)
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;
// Usage
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 (every N-th element)
Traverse every N-th element.
#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;
// Usage: prints 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 (minimal)
Prefer the standard std::views::transform.
// Prefer standard 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-based range examples
A sentinel lets end() return a different type than the iterator. When you do not need a full end iterator, you can save memory or comparison work.
Null-terminated C string
A C string ends at '\0'. With a sentinel you can detect the end with *it == '\0' instead of scanning to compare iterators.
#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: different type from the iterator
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{}; }
};
// Usage
int main() {
CStringRange r("Hello");
for (char c : r)
std::cout << c; // Hello
auto count = std::ranges::count(r, 'l'); // 2
}
Line-by-line file reading (sentinel)
Treat EOF like a sentinel, similar in spirit to std::istream iteration.
#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; // read first line
}
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{}; }
};
// Usage: for (const auto& line : LineRange(std::cin)) { ....}
Why use sentinels?
- Memory:
end()can return a lightweight empty sentinel - Comparison:
it == sentinelcan callit.is_end()once - Infinite ranges: A sentinel can mean “never equal” for unbounded sequences
5. Common mistakes and fixes
Issue 1: Iterator requirements not met
Symptom: std::ranges::sort(my_range) fails — “does not satisfy random_access_iterator”
Cause: sort needs random_access_range. input_iterator alone is not enough.
Fix:
// ❌ Only input_iterator → no sort
using iterator_category = std::input_iterator_tag;
// ✅ Provide random_access_iterator
using iterator_category = std::random_access_iterator_tag;
// Add operator+, operator-, operator[], operator<, ...
Issue 2: Concept check fails
Symptom: static_assert(std::ranges::range<MyRange>) fails
Cause: begin()/end() do not return types that model std::input_or_output_iterator, or the sentinel cannot be compared with begin()’s iterator.
Fix:
// ❌ begin/end return void or wrong types
void begin() const;
// ✅ Return iterators (or sentinels)
auto begin() const { return iterator(...); }
auto end() const { return iterator(...); } // or sentinel
Issue 3: const correctness
Symptom: Error when calling r.begin() on const MyRange&
Cause: begin()/end() are not const.
Fix:
// ❌ Cannot call begin on const
auto begin() { return ...; }
// ✅ const member functions
auto begin() const { return ...; }
auto end() const { return ...; }
Issue 4: Iterator invalidation
Symptom: Undefined behavior while iterating because the container changed
Cause: Reallocation (e.g. vector::push_back) invalidates iterators.
Fix: Do not mutate the underlying container during traversal, or document lifetime rules clearly (as with span).
Issue 5: Predicate pointer/reference lifetime
Symptom: FilterView stores a lambda; iterators use Pred* pred_; after moving the view, iterators dangle.
Cause: Iterator holds a pointer into moved storage.
Fix:
// ✅ Avoid copying views unsafely, or store the predicate inside the iterator
// or use std::reference_wrapper for the base range only
Issue 6: iterator_reference_t vs value_type
Symptom: transform view’s operator* returns T but traits say T&.
Cause: When operator* returns a proxy or temporary, separate reference from value_type.
Fix:
// ✅ In C++20, iterator_traits often deduce from operator*
// Align value_type, reference, and pointer explicitly when needed
using value_type = std::remove_cvref_t<std::iter_reference_t<Iterator>>;
Issue 7: Template instantiation errors
Symptom: FilterView<std::vector<int>, SomePred> — incomplete type / undefined type
Cause: Lambdas are not default-constructible; copy paths may fail.
Fix:
// ✅ Move Pred consistently
FilterView(R r, Pred p) : base_(std::move(r)), pred_(std::move(p)) {}
// closures: std::move(p)
Issue 8: Rvalue range lifetime
Symptom: Crash after auto v = get_temporary_vector() | filter(pred);
Cause: The view stores a reference to a temporary that is already destroyed.
Fix:
// ✅ Traverse the temporary immediately
for (auto x : get_temporary_vector() | filter(pred)) { ....}
// Or design the view to own/move the range
Issue 9: operator== symmetry
Symptom: it == sentinel works but sentinel == it does not compile
Cause: Only one side of operator== is defined.
Fix:
// ✅ Define both directions (e.g. as friends)
friend bool operator==(const Iterator& it, Sentinel s) { return it.is_end(); }
friend bool operator==(Sentinel s, const Iterator& it) { return it.is_end(); }
Issue 10: Compatibility with default_sentinel_t
Symptom: Errors comparing to std::default_sentinel_t
Cause: Some views use default_sentinel; your iterator must compare if you interop.
Fix:
// ✅ Optionally add operator== with default_sentinel_t
friend bool operator==(const Iterator& it, std::default_sentinel_t) {
return it.is_end();
}
6. Best practices
iterator_concept vs iterator_category
In C++20, iterator_concept takes precedence over iterator_category. For std::random_access_iterator, define 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
Views should usually be cheap to copy to model std::ranges::view. Copying should copy handles/references, not large buffers.
// ✅ Hold the base range by reference — copying copies the handle
template <typename R>
class MyView {
R base_;
public:
MyView(R r) : base_(std::move(r)) {}
};
noexcept
Mark operator++ and operator* noexcept when safe so algorithms can optimize.
T& operator*() const noexcept { return *ptr_; }
Iterator& operator++() noexcept { ++ptr_; return *this; }
Document rules
- Lifetime: Does the range reference external storage or own it?
- Invalidation: When do iterators become invalid?
- Exceptions: Can
begin()/end()throw?
7. Performance comparison
Benchmark scenario
- Data: 1,000,000 integers
- Work: filter evens, then sum
| Approach | Relative time | Memory |
|---|---|---|
vector + manual loop | 1.0× (baseline) | N * sizeof(int) |
views::filter (lazy) | ~1.0× | O(1) extra |
Push results into another vector | ~1.2× | ~2N |
Custom FilterView | ~1.0× | O(1) extra |
Takeaways:
- Lazy views avoid intermediate containers.
- Pipelines
v | filter(p) | transform(f)can fuse into a single pass. - For custom ranges, avoid virtual calls and redundant copies in hot iterators.
Optimization tips
// ✅ Pass by reference (avoid copies)
template <std::ranges::input_range R>
class MyView {
R base_;
public:
explicit MyView(R&& r) : base_(std::forward<R>(r)) {}
};
// ✅ Cache in the iterator when the same value is read repeatedly
8. Production patterns
Lazy evaluation
Views compute only when iterated. Building a pipeline costs nothing until you loop.
auto pipeline = v
| std::views::filter([](auto x) { return x > 0; })
| std::views::transform([](auto x) { return x * 2; });
// No work yet
for (auto x : pipeline) { // work happens here
// ...
}
Infinite range
For endless sequences, make the sentinel never match; use take to bound iteration (like std::views::iota).
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; } // never ends
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{}; }
};
// Usage: bound with take
// for (auto x : InfiniteIota(0) | std::views::take(10))
Composable pipeline
Reuse a pipeline across ranges.
auto positive_doubled = [](auto&& r) {
return std::forward<decltype(r)>(r)
| std::views::filter([](auto x) { return x > 0; })
| std::views::transform([](auto x) { return x * 2; });
};
std::vector<int> v = {-1, 2, -3, 4};
for (auto x : positive_doubled(v))
std::cout << x << " "; // 4 8
Concatenating ranges
Join multiple ranges. In C++23 you can use std::ranges::views::concat.
#include <ranges>
#include <vector>
#include <iostream>
// C++23: v1 | std::views::concat(v2) | std::views::concat(v3)
// Manual: variant or an iterator that switches between two ranges
Type erasure
To treat different range types uniformly you might use std::any or virtual calls—usually slower. Prefer std::variant or templates when you can.
9. Using view_interface
Inherit to get view helpers
Inheriting std::ranges::view_interface<MyView<R>> can yield empty(), operator bool, size() (for random_access_range), and sometimes data() when you only implement begin / end.
#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(), etc. become available where applicable
10. Worked examples
Index range (Iota-style)
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 << " ";
Slice (start index, count)
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_;
}
};
Making SensorBuffer a range (fixing the opening example)
#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(); }
// ✅ Add begin/end → satisfies 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. Implementation checklist
When you build a custom range, verify:
-
begin()/end()(or sentinel) provided - Iterator implements
operator*,operator++,operator== -
iterator_categoryoriterator_conceptdefined -
begin()/end()callable onconstobjects -
static_assert(std::ranges::range<MyRange>)passes - Lifetime documented (especially for non-owning ranges)
-
random_access_iteratorif you needranges::sort - Adaptors use
operator|and a closure object - With
view_interface,begin/endmay be enough forempty, etc.
Related posts (internal)
- C++20 Ranges | Escaping the begin/end loop and using ranges algorithms
- C++ Ranges — a C++20 functional-style guide
- C++ custom iterators — Forward and Bidirectional
Keywords
Search terms that match this article: C++ custom range, range adaptor, iterator, implementing ranges, sentinel, view_interface.
Summary
| Topic | Meaning |
|---|---|
| range | Provides begin/end (or sentinel) |
| Iterator | ++, *, ==/!=, traits-compatible |
| sentinel | End type comparable with the iterator |
| Range adaptor | Chaining with pipe | |
| view_interface | Derive empty, size, … |
| Lazy / infinite | Compute on traversal; bound with take |
FAQ (inline)
Q. When do I use this at work?
A. Whenever you want domain containers in for (auto x : …) or ranges:: algorithms, or when you build views/adaptors over existing ranges. Exposing log buffers, sensor feeds, or packet streams as ranges simplifies code.
Q. What should I read first?
A. Follow the previous post links at the bottom of each article in order, or use the C++ series index for the full path.
Q. Where can I go deeper?
A. See cppreference — Ranges library and the Ranges draft.
One-line recap: Get begin/end (or sentinel) right and your type can be a range. Range adaptors plus lazy/infinite patterns cover most real-world uses.
Previous: [C++ Hands-On #25-2] Views and pipelines
Next: [C++ Hands-On #26-1] constexpr functions and variables
More related posts
- C++20 Ranges | Escaping the begin/end loop and using ranges algorithms
- C++ Ranges views and pipelines | Lazy evaluation [#25-2]
- C++20 Modules
- Migrating a legacy project to Modules | step-by-step [#24-2]
- C++ constexpr functions and variables | Compute at compile time [#26-1]