Ranges: When Abstraction Becomes Obstruction
Or: the process is the punishment
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
Finding nullopt in a Range of Optionals
The most striking example involves only standard library types:
(view at https://godbolt.org/z/M1o5z7r7M)
#include <vector>
#include <ranges>
#include <optional>
int main() {
std::vector<std::optional<int>> v = {{1}, std::nullopt, {3}};
auto it = std::ranges::find(v, std::nullopt); // fails to compile
}Result: Constraint failure in std::ranges::find.
The natural way to express “find an empty optional” doesn’t work. Users must instead write:
auto it = std::ranges::find(v, std::optional<int>{});This compiles but obscures intent. The failure occurs because std::nullopt_t doesn’t satisfy equality_comparable with itself—it lacks operator==(nullopt_t, nullopt_t).
Cross-Type Optional Comparison
A similar issue appears when optional types differ only in their value type:
(view at https://godbolt.org/z/ExGneMsGM)
#include <vector>
#include <ranges>
#include <optional>
int main() {
std::vector<std::optional<int>> v = {{0}, {1}, {2}};
auto it = std::ranges::find(v, std::optional{0L}); // fails to compile
}Result: Constraint failure due to missing common_reference_t<optional<int>&, optional<long>&>.
The optionals are comparable via operator==—the comparison is well-formed and meaningful—but the concept constraint rejects it.
User-Defined Types with Heterogeneous Equality
Consider a network application searching a buffer by sequence number:
(view at https://godbolt.org/z/G6rjPxo7v)
#include <vector>
#include <ranges>
struct Packet {
int seq_num;
bool operator==(int seq) const { return seq_num == seq; }
};
int main() {
std::vector<Packet> packets{{1001}, {1002}, {1003}};
auto it = std::ranges::find(packets, 1002); // fails to compile
}Result: Constraint failure in std::ranges::find.
The Packet type explicitly provides operator== for comparison with int. The code expresses clear intent. Yet std::ranges::find rejects it because equality_comparable_with<Packet, int> requires common_reference_t<Packet&, int&>, which doesn’t exist.
Contrast: The pre-ranges approach works perfectly:
auto it = std::find(packets.begin(), packets.end(), 1002); // worksThe Pattern
Each failure stems from the same root: equality_comparable_with requires a common reference type that doesn’t exist, despite operator== being well-formed. In every case:
The comparison expresses clear, natural intent
The
operator==is well-defined and correctHand-written loops work perfectly
std::findworks perfectlystd::ranges::findrefuses to compile
The newer abstraction is less useful than the facility it was designed to improve.
Understanding the Constraint
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.
The challenge is that these constraints reject legitimate use cases. When a type author explicitly provides operator== for heterogeneous comparison, that represents a deliberate design decision. The constraint second-guesses that decision by requiring additional type relationships that may not exist and may not be necessary.
The Practitioner’s Perspective
Online discussion of this paper produced a spirited exchange that crystallized the tension between theoretical purity and engineering pragmatism. When practitioners expressed frustration at needing to model “perfect regular transitive symmetric submanifolds” just to search a collection, library maintainers responded that heterogeneous operator== constitutes an “abuse of operator overloading.”
The practitioners’ rebuttal was illuminating: engineers solve problems, they don’t contemplate abstract theory. If comparing packets to sequence numbers gets the job done, that’s what engineers do. And if operator<< can be repurposed for I/O without the sky falling, perhaps heterogeneous equality isn’t the catastrophe it’s made out to be. The suggested workaround—projections—sounds elegant in conference talks, but in production code &SomeLongNameSpace::SomeLongClassName::SomeLongMethodName is rather less charming than the slides suggest.
More pointedly: when you’re integrating a third-party library whose string type lacks std::string conversion (because the authors understand C++ has no stable ABI), you cannot add common_reference specializations. You can define operator==, and std::find works beautifully. But ranges::find refuses, demanding type relationships you cannot provide for types you do not own.
There’s also a certain irony in a library that requires common_reference for ranges::find while the committee simultaneously adds heterogeneous lookup to associative containers—which is fundamentally the same operation of comparing apples to oranges. The constraint appears less like a principled requirement and more like an accident of the design process that nobody has gotten around to fixing.
Path Forward
Consider that this works:
std::vector<std::optional<int>> v = {{1}, std::nullopt, {3}};
for (auto it = v.begin(); it != v.end(); ++it)
if (*it == std::nullopt) break;We could bridge the gap 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 retains 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_withshould not requirecommon_reference_twhen direct comparison viaoperator==provides well-defined semantics. The constraint would accept either shared common reference or well-formed direct comparison.
This would enable:
Finding
std::nulloptin ranges of optionalsCross-type optional comparisons (
optional<int>vsoptional<long>)User-defined types with heterogeneous
operator==
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
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 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 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 have legitimate claims.
Acknowledgments
Thanks to Peter Dimov for insightful observations about the current state of Ranges. Thanks to contributors on Reddit’s r/cpp who identified the nullopt and cross-type optional examples, and who pointed out errors in earlier versions of this paper. Thanks to the Ranges working group for their sustained effort on one of C++’s most ambitious library additions.
Revision History
2025-12-24 — Replaced incorrect examples with verified failing cases; added nullopt and optional examples; removed zip view example (compiles in C++23); added “Practitioner’s Perspective” section; credited Reddit contributors
2025-12-16 — Initial draft


"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
```
Poor article.
> The Packet type provides symmetric operator== overloads for comparison with std::uint32_t. This is a natural design [...]
Bad premise and bad design. Equality on `Packet` should *naturally* compare the value/contents of the packet, not one arbitrary member.
I would never let this code get through review.
auto it = std::ranges::find_if(rx_buffer,
[](Packet const& p) { return p == 1002; });
Yep -- did you seriously write this and felt like *"yeah, `p == 1002` seems like reasonable code"*?
struct User {
std::string name;
int id;
friend bool operator==(User const& u, int user_id) noexcept {
return u.id == user_id;
}
};
Again, redefining equality to completely ignore `name`. So `User{"Bob", 10} == User{"Alice", 10}`. Boo!
> The same issue appears with fundamental types and standard library types: [...]
This is a much more compelling example. Too bad that [it compiles and works](https://gcc.godbolt.org/z/dr3WrYY5G), despite you claiming otherwise.
**Testing your code snippets is the bare minimum before writing a blog post.**
There are so many valid things to critique about ranges (e.g. compile time bloat, poor debuggability, poor debug performance) and yet you pick (1) terrible premises and (2) incorrect examples?