Ranges: When Abstraction Becomes Obstruction
Or: the process is the punishment
Document number: P9999R0
Date: 2025-12-16
Project: Programming Language C++
Audience: LEWG, SG9
Reply-to: vinnie.falco@gmail.com
Abstract
The Ranges library represents one of C++’s most ambitious modernization efforts, yet its over-constrained concept requirements prevent straightforward use cases that trivial hand-written code handles effortlessly. We document concrete examples where std::ranges encounters unexpected usability barriers, examine the design philosophy that led us here, and propose a path forward that respects both theoretical foundations and the practical needs of production code.
Introduction
The Ranges library is, by all accounts, “mostly fine”—and yet this qualified endorsement from our most experienced practitioners suggests an opportunity for improvement. This paper documents specific cases where “mostly” falls short of “usable,” and explores how we might close that gap.
We built Ranges to make common operations easier and more expressive. In many ways, we succeeded. However, certain design decisions have created unexpected friction for operations that users reasonably expect to work.
The Problem
Consider a network application that needs to search a buffer of received packets by sequence number:
#include <span>
#include <ranges>
#include <algorithm>
#include <cstdint>
struct Packet
{
friend bool operator==(Packet const& p, std::uint32_t seq) noexcept {
return p.seq_num_ == seq;
}
friend bool operator==(std::uint32_t seq, Packet const& p) noexcept {
return seq == p.seq_num_;
}
std::uint32_t seq_num_ = 0;
};
int main()
{
Packet rx_buffer[] = { {1001}, {1002}, {1003} };
auto it = std::ranges::find(rx_buffer, 1002);
}
Result: Constraint failure in std::ranges::find.
The Packet type provides symmetric operator== overloads for comparison with std::uint32_t. This is a natural design: packets are frequently searched by sequence number, and the type explicitly supports this operation in both argument orders. The code expresses clear intent and follows established C++ idioms.
Yet std::ranges::find rejects this code. The failure occurs because ranges::find uses ranges::equal_to, which requires equality_comparable_with<Packet, std::uint32_t>. This concept demands common_reference_t<Packet&, std::uint32_t&>, which does not exist. The constraint embodies a theoretically sound principle: we seek “regular” semantics where equality is transitive and symmetric across a well-defined common type.
The challenge is that Packet and std::uint32_t are comparable through the explicitly-provided operator== overloads. The current design inadvertently prevents this natural usage pattern despite the type author’s explicit intent to support it.
Users must write:
auto it = std::ranges::find_if(rx_buffer,
[](Packet const& p) { return p == 1002; });
Or revert to the pre-ranges approach:
auto it = std::find(std::begin(rx_buffer), std::end(rx_buffer), 1002);
This creates a situation where the newer abstraction is less immediately useful than the facility it was designed to improve upon.
Scope of the Issue
This pattern affects heterogeneous comparisons throughout the library. Any time a user-defined type provides operator== for comparison with a different type—a common and well-established design pattern—the ranges algorithms reject the comparison:
struct User {
std::string name;
int id;
friend bool operator==(User const& u, int user_id) noexcept {
return u.id == user_id;
}
};
std::vector<User> users = {{”Alice”, 1}, {”Bob”, 2}};
std::ranges::find(users, 2); // fails: no common_reference_t<User&, int&>
The same issue appears with fundamental types and standard library types:
std::vector<int> v = {1, 2, 3};
std::ranges::find(v, 1L); // fails: no common_reference_t<int&, long&>
std::vector<std::string_view> views = {”foo”, “bar”};
std::string target = “bar”;
std::ranges::find(views, target); // fails: no common_reference_t<string_view&, string&>
Each stems from the same root: equality_comparable_with requires a common reference type that doesn’t exist despite operator== being well-formed. These are all cases where the language’s comparison semantics work correctly, but our library concepts prevent their use.
Similar philosophical decisions affect other comparison facilities. These choices were made with careful consideration of theoretical soundness, and we should acknowledge the genuine value in striving for mathematically rigorous semantics. The question is whether we’ve struck the right balance between theoretical elegance and practical utility.
Understanding the Challenge
This situation reflects a broader challenge in standards work: reconciling competing priorities within our process. The equality_comparable_with design embodies real insights about cross-type equality semantics. The requirement for a common reference provides a principled foundation for reasoning about symmetric, transitive equality.
At the same time, shipping a feature involves navigating multiple working groups, achieving consensus among experts with different priorities, and maintaining backward compatibility with existing code. Once a feature reaches a certain stage in our pipeline, course corrections become increasingly difficult—not because anyone lacks technical capability, but because our process is designed (quite reasonably) to ensure stability and prevent churn.
This creates an inherent tension: features must be “good enough to ship” within the time available, yet “shipped features” are very difficult to revise even when usage reveals opportunities for improvement. The result is that we sometimes lock in designs that, while locally optimal given the constraints of the standardization process, prove less than optimal when exposed to real-world usage patterns.
Path Forward
Consider that this works:
Packet rx_buffer[] = { {1001}, {1002}, {1003} };
auto it = std::begin(rx_buffer);
for (; it != std::end(rx_buffer); ++it)
if (*it == 1002) break;
Five lines. Works for every case where operator== is well-formed. We could bridge the gap between theoretical foundations and practical needs by allowing equality_comparable_with to succeed when either:
The types share a common reference (preserving theoretical rigor where applicable), or
Direct heterogeneous comparison via
operator==is well-formed (enabling practical usage where appropriate)
This approach would retain the conceptual framework while acknowledging that explicitly-provided comparison operators represent legitimate equality semantics.
Proposal
We propose relaxing the constraints on ranges::equal_to and related comparison predicates to permit heterogeneous comparisons when operator== is well-formed. Specifically:
equality_comparable_with should not require common_reference_t when direct comparison via operator== provides well-defined semantics. The constraint would accept either shared common reference or well-formed direct comparison.
This would enable:
Arrays of
Packetto be searchable withstd::uint32_tsequence numbersvector<User>to be searchable withintIDsvector<string_view>to be searchable withstringvaluesvector<int>to be searchable withlongvaluesCountless other combinations that users reasonably expect based on existing language semantics
The principle: where theoretical elegance and practical utility both have merit, we should seek designs that serve both rather than requiring a binary choice.
Process Observations
This situation offers valuable insights into how we might improve our standardization process:
Earlier Real-World Validation: Features that affect daily usage patterns benefit from extensive implementation experience before standardization. Technical Specifications serve this purpose well, but even broader deployment—perhaps through widely-used libraries or compiler extensions—could reveal usability issues earlier.
Streamlined Correction Mechanisms: When shipped features prove problematic in practice, we currently face substantial procedural barriers to correction. Developing lighter-weight mechanisms for addressing clear usability issues—while maintaining our essential stability guarantees—could help us respond more effectively to user feedback.
Balancing Rigor and Pragmatism: Our pursuit of theoretical soundness has produced many of C++’s strengths. We might benefit from explicitly prioritizing practical usability in cases where both theoretical soundness and established language semantics (like explicitly-provided comparison operators) have legitimate claims.
These observations aren’t criticisms of individuals or groups—everyone involved is working thoughtfully within a complex system. Rather, they suggest opportunities to evolve our process to better serve both the theoretical foundations and practical needs of the language.
Acknowledgments
Thanks to Peter Dimov for insightful observations about the current state of Ranges. Thanks to the Ranges working group for their sustained effort on one of C++’s most ambitious library additions—the challenges identified here are natural consequences of pioneering work, not failures of effort or expertise.
Thanks to all committee members who continue to grapple with the inherent difficulty of standardizing features that must simultaneously satisfy theoretical rigor, practical utility, implementation feasibility, and the diverse needs of our global user community. This work is genuinely hard, and we propose these refinements in the spirit of collaborative improvement.
We recognize that addressing this issue will require careful work through our existing process. We hope this paper can serve as a constructive starting point for that discussion.


"The Problem" indeed exists in real-word code, but imo it's caused by bad coding styles. Using `bool operator==(element_t, std::uint32_t)` is a temporary solution and causes confusion to readers including the future author himself.
I would rather keep my code "clean":
1. Make projection (loss of information) explicit, since an object is-not-equal-to its member (in information theory sense);
2. Avoid comparing values of different types.
```
std::ranges::find(elements, what_to_find, &element_t::member);
std::ranges::find(ints, static_cast<int>(a_long)); // value truncated, dangerous
std::ranges::find(ints | std::transform(turn_int_to_long), a_long); // value preserved, safe
std::ranges::find(longs, static_cast<long>(a_int)); // elements preserved, safe
std::ranges::find(longs | std::transform(turn_long_to_int), a_int); // elements truncated, dangerous
```