How to Make Proxy References and Iterators Satisfy C++20 Iterator Concepts
Writing custom iterators in modern C++ used to be straightforward: define iterator_traits, return true references (T&), and you were done. However, building custom containers with proxy references—where dereferencing yields a transient proxy object instead of an existing lvalue reference—frequently trips over C++20 iterator concepts such as std::forward_iterator.
Understanding the Problem: The Dreaded common_reference Failure
In traditional C++98/C++03, a forward iterator’s reference type had to be a true reference (value_type&). C++20’s Ranges library relaxed this requirement to support proxy iterators (like std::vector<bool>::reference and std::views::zip).
However, C++20 concepts enforce strict semantic modeling through std::indirectly_readable. To satisfy std::forward_iterator, your iterator must satisfy:
std::common_reference_with<std::iter_reference_t<It>, std::iter_value_t<It>&>When *it returns a proxy object rather than a true lvalue reference to value_type, std::common_reference fails because the compiler cannot deduce an underlying reference type common to both proxy_reference&& and value_type&.
Why Did Specializing basic_common_reference Cause Compiler Errors?
A frequent attempt to fix this issue is specializing std::basic_common_reference directly for the proxy reference:
template<template<class> class TQual, template<class> class UQual>
struct std::basic_common_reference<proxy_ref, val_type, TQual, UQual> {
using type = proxy_ref;
};This causes errors like:
note: the expression 'is_convertible_v<const value_type&, proxy_reference>' evaluated to 'false'The reason lies in the definition of std::common_reference_with<A, B>. It requires both std::convertible_to<A, Common> and std::convertible_to<B, Common>. If Common is defined as non-const proxy_reference, converting const value_type& into proxy_reference fails because proxy_reference binds to non-const T&, violating const-correctness.
The Solution: Tuple-Like Customization and std::tuple
In C++23 (via P2165R4), std::tuple and std::pair gained full support for proxy reference comparisons and common references via std::tuple_like. Instead of rolling your own custom struct that manually tries to resolve reference types, you have two idiomatic paths:
Approach 1: Use std::tuple<std::size_t, T&> as the Reference Type
The cleanest and most robust modern solution is using a std::tuple of references. std::tuple has built-in specialization for std::basic_common_reference in modern standard libraries:
#include <vector>
#include <tuple>
#include <iterator>
#include <cstddef>
template <typename T>
struct container {
using value_type = std::tuple<std::size_t, T>;
// Proxy reference: a tuple of references
using proxy_reference = std::tuple<std::size_t, T&>;
struct proxy_iterator {
using iterator_category = std::forward_iterator_tag;
using iterator_concept = std::forward_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = container::value_type;
using reference = proxy_reference;
proxy_reference operator*() const;
proxy_iterator& operator++();
proxy_iterator operator++(int);
bool operator==(const proxy_iterator&) const;
};
std::vector<T> _data;
};
// Static assertion passes out-of-the-box in C++23!
static_assert(std::forward_iterator<container<int>::proxy_iterator>);Approach 2: Full Specialization of basic_common_reference
If you must use a dedicated struct for your proxy reference, your proxy needs a matching const-qualified proxy reference type (e.g., const_proxy_reference containing const T&). The common_reference must resolve to the qualified reference type depending on TQual and UQual:
#include <type_traits>
#include <utility>
template <typename T>
struct proxy_ref {
std::size_t first;
T& second;
// Convertible to value_type
operator std::pair<std::size_t, T>() const {
return {first, second};
}
};
template <typename T>
struct const_proxy_ref {
std::size_t first;
const T& second;
const_proxy_ref(proxy_ref<T> p) : first(p.first), second(p.second) {}
const_proxy_ref(const std::pair<std::size_t, T>& p) : first(p.first), second(p.second) {}
};
// Specialize std::basic_common_reference
template <typename T, template <class> class TQual, template <class> class UQual>
struct std::basic_common_reference<proxy_ref<T>, std::pair<std::size_t, T>, TQual, UQual> {
using type = const_proxy_ref<T>;
};
template <typename T, template <class> class TQual, template <class> class UQual>
struct std::basic_common_reference<std::pair<std::size_t, T>, proxy_ref<T>, TQual, UQual> {
using type = const_proxy_ref<T>;
};Key Takeaways
- Always define
iterator_concept = std::forward_iterator_tag;to explicitly communicate C++20 range capabilities. - A proxy reference cannot merely return non-const references when asked for a common reference with a
const value_type&. - Prefer standard components like
std::tuple<Refs...>in C++23. They integrate seamlessly with standard concepts without requiring fragile boilerplate.