C++ Custom Ranges
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.
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>);
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
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
}
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
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();
}
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?
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
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.
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
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);
}
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.
Range pieces and what each needs
| 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. Do I have to inherit from view_interface?
A. No. It reduces boilerplate when you build views (you get helpers such as empty(), operator bool and, where applicable, size() from your begin/end), but it is not mandatory for every custom range.
Q. How do I combine a custom range with std::views adaptors?
A. Make sure your iterators expose the right category and satisfy the iterator concepts. Incomplete iterators can fail at compile time or cause undefined behavior when composed with adaptors, so add a static_assert(std::ranges::range<MyRange>) (or the stronger concept you need) early.
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.
Next: [C++ Hands-On #26-1] constexpr functions and variables
More related posts
Related Articles
- C++ Range-Based for: auto, References, Temporaries,
- C++ string fundamentals — std::string, C strings,
- C++ std::vector Basics — Initialization, Operations &