std::ranges::equal vs std::equal Safety and Size Checks
The introduction of the Ranges library in C++20 modernizes standard
algorithms to eliminate common memory safety pitfalls and streamline
code. This article examines the differences between legacy
std::equal and std::ranges::equal,
specifically focusing on how the modern ranges implementation guarantees
range-bound safety, enforces proper size validation, and optimizes
comparisons using C++ concepts.
The Vulnerability of
Legacy std::equal
Prior to C++14, the standard std::equal algorithm
required three iterators: two defining the range of the first sequence
(first1, last1) and only one denoting the
beginning of the second sequence (first2):
std::equal(first1, last1, first2);This three-iterator signature relies on a critical precondition: the
sequence starting at first2 must contain at least as many
elements as the range [first1, last1). If the second range
is shorter, the algorithm continues incrementing first2
past its valid boundary, causing an out-of-bounds read and resulting in
undefined behavior.
Although C++14 introduced a four-iterator overload
(first1, last1, first2, last2), the legacy three-iterator
signature remains in <algorithm> for backward
compatibility. Because it remains available, developers can still
introduce silent buffer overreads through simple omissions.
How
std::ranges::equal Enforces Safety
Defined in the <algorithm> header under the
std::ranges namespace, std::ranges::equal
completely drops the dangerous single-iterator destination pattern. It
requires explicit boundaries for both inputs, either by passing ranges
directly or by providing an iterator-sentinel pair for each
sequence:
// Range-based syntax
std::ranges::equal(range1, range2);
// Iterator-sentinel syntax
std::ranges::equal(first1, last1, first2, last2);By requiring the boundaries of both sequences at compile time,
std::ranges::equal prevents the second range from being
traversed beyond its end. If range2 is shorter than
range1, the algorithm safely stops and returns
false instead of reading past the end of the container.
Automatic and Optimized Size Checking
Beyond eliminating out-of-bounds reads,
std::ranges::equal implements structural size checks using
modern C++ concepts:
- Short-Circuit via
std::ranges::sized_range: If both inputs satisfy thestd::ranges::sized_rangeconcept (such asstd::vector,std::array, orstd::span), their sizes can be determined in \(O(1)\) constant time.std::ranges::equalchecks ifstd::ranges::distance(range1) == std::ranges::distance(range2)before inspecting any elements. If the sizes do not match, the algorithm immediately returnsfalsewithout traversing the contents. - Support for Non-Sized Ranges and Sentinels: For
ranges where size is not known in advance (such as a stream or a
null-terminated string represented via a custom sentinel), the algorithm
steps through both ranges simultaneously. If either iterator reaches its
respective sentinel before the other, the comparison terminates safely
and yields
false.
Key Differences Summary
| Feature | Legacy std::equal |
std::ranges::equal |
|---|---|---|
| Unbounded Overload | Supported (3-iterator version, prone to undefined behavior) | Prohibited (requires full bounds for both ranges) |
| Direct Container Support | Requires .begin() and
.end() calls |
Directly accepts range objects |
| Sentinels | Requires both ends to share the same iterator type | Works with distinct sentinel types |
| Size-Check Short-Circuit | Ad-hoc optimization based on iterator categories | Concept-driven via
std::ranges::sized_range |