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:

  1. Short-Circuit via std::ranges::sized_range: If both inputs satisfy the std::ranges::sized_range concept (such as std::vector, std::array, or std::span), their sizes can be determined in \(O(1)\) constant time. std::ranges::equal checks if std::ranges::distance(range1) == std::ranges::distance(range2) before inspecting any elements. If the sizes do not match, the algorithm immediately returns false without traversing the contents.
  2. 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