Dangling Iterators and std::ranges::dangling in C++
A dangling iterator in C++ points to memory that has been
deallocated, which leads to undefined behavior if dereferenced. This
issue frequently occurs when an algorithm operates on a temporary
container (an rvalue) that ceases to exist at the end of the full
expression. To eliminate this category of runtime bugs, C++20 introduced
std::ranges::dangling, a placeholder type that replaces the
expected iterator return type at compile time whenever an algorithm is
invoked with a non-borrowed temporary range.
What Is a Dangling Iterator?
An iterator is an abstraction representing a position within a range or container. An iterator becomes "dangling" when the underlying data structure it points to is destroyed or modified in a way that invalidates its memory references.
In traditional C++ (prior to C++20), a common pitfall involved passing a temporary container to a utility or algorithm:
#include <vector>
#include <algorithm>
std::vector<int> get_data() {
return {1, 2, 3, 4, 5};
}
// Undefined Behavior:
auto it = std::find(get_data().begin(), get_data().end(), 3);
// get_data() creates two distinct temporary vectors.
// Even if called once, the temporary vector dies at the semicolon,
// leaving 'it' pointing to deallocated memory.Dereferencing it produces undefined behavior, which
often passes undetected during testing because the memory might not be
immediately overwritten.
The C++20 Ranges Approach
C++20 algorithms in the std::ranges namespace operate
directly on whole ranges rather than iterator pairs. For example, you
can write:
auto result = std::ranges::find(get_data(), 3);Because get_data() returns a temporary
std::vector<int>, returning a valid iterator from
std::ranges::find would still leave the caller with an
invalid pointer as soon as the expression finishes evaluating.
Instead of allowing this bug to manifest at runtime, C++20 introduces
the concept of borrowed ranges and the tag type
std::ranges::dangling.
What Is std::ranges::dangling?
std::ranges::dangling is an empty utility class defined
in the <ranges> header:
namespace std::ranges {
struct dangling {
constexpr dangling() noexcept = default;
template <typename... Args>
constexpr dangling(Args&&...) noexcept {}
};
}It is not an iterator; it defines no operator*,
operator++, operator->, or comparison
operators. It exists solely to absorb returned iterators from
expressions involving temporary containers.
How Compile-Time Prevention Works
When an algorithm in std::ranges executes, it checks
whether the provided range satisfies the concept
std::ranges::borrowed_range. A borrowed range is a range
whose iterators remain valid even after the range object itself has been
destroyed.
- Lvalues: If you pass an lvalue (such as a named
variable
std::vector<int> vec), the container will outlive the algorithm call. The algorithm returns a standard iterator. - Borrowed Rvalues: If you pass an rvalue that is
safe to borrow from (such as
std::string_vieworstd::span), the algorithm also returns a standard iterator because views do not own their underlying storage. - Non-Borrowed Rvalues: If you pass an rvalue
container that owns its elements (such as a temporary
std::vectororstd::string), the algorithm returnsstd::ranges::danglinginstead of an iterator.
Consider the following example:
#include <vector>
#include <ranges>
#include <algorithm>
std::vector<int> get_numbers() {
return {10, 20, 30};
}
int main() {
// get_numbers() returns a non-borrowed rvalue
auto result = std::ranges::find(get_numbers(), 20);
// 'result' is of type std::ranges::dangling
// The following line causes a compile-time error:
// int val = *result;
}Because result is of type
std::ranges::dangling, attempting to dereference it via
*result causes the compiler to fail immediately with an
error stating that operator* is not defined for
std::ranges::dangling.
Safe Alternatives
To safely capture the result of a range algorithm, ensure the container's lifetime extends beyond the algorithm's execution:
auto numbers = get_numbers(); // Named lvalue
auto it = std::ranges::find(numbers, 20); // Returns std::vector<int>::iterator
if (it != numbers.end()) {
int value = *it; // Safe: 'numbers' is still in scope
}By substituting unsafe iterators with
std::ranges::dangling, C++ converts what was once an
insidious source of undefined behavior into an immediate, explicit
compile-time error.