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.