C++ Custom Ranges | Building Types That Satisfy the Range Concept [#25-3]

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 LogBuffer and process only the last N logs, but without begin/end you are stuck with manual indexing.
  • Network packet stream: You want ranges::find_if on PacketStream, but it is not a range so algorithms do not apply.
  • Slicing: You want to pass only part of a vector to ranges::sort without copying everything—without subrange you 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_interface so 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

  1. Range requirements
  2. Complete custom range implementation
  3. Range adaptor implementation
  4. Sentinel-based range examples
  5. Common mistakes and fixes
  6. Best practices
  7. Performance comparison
  8. Production patterns
  9. Using view_interface
  10. Worked examples
  11. 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) and end(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 required
  • iterator_category affects algorithm selection (e.g. random_access_iterator enables ranges::sort)
  • begin() and end() 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 == sentinel can call it.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
ApproachRelative timeMemory
vector + manual loop1.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_category or iterator_concept defined
  • begin() / end() callable on const objects
  • static_assert(std::ranges::range<MyRange>) passes
  • Lifetime documented (especially for non-owning ranges)
  • random_access_iterator if you need ranges::sort
  • Adaptors use operator| and a closure object
  • With view_interface, begin/end may be enough for empty, etc.


Keywords

Search terms that match this article: C++ custom range, range adaptor, iterator, implementing ranges, sentinel, view_interface.

Summary

TopicMeaning
rangeProvides begin/end (or sentinel)
Iterator++, *, ==/!=, traits-compatible
sentinelEnd type comparable with the iterator
Range adaptorChaining with pipe |
view_interfaceDerive empty, size, …
Lazy / infiniteCompute 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