January 7, 2020·4 min read·engineeringcpprangeslifetime

Temporary range adapters do not always invalidate iterators

P2017R0 proposes propagating iterator-lifetime properties through range adapters. A temporary wrapper and the storage it references can have different lifetimes.

Destroying a range wrapper need not destroy the elements reached through its iterators. A wrapper might only refer to storage owned elsewhere. Barry Revzin's January 7 paper P2017R0 identifies cases where the C++ ranges design rejects useful iterators because it treats an adapter as unconditionally unsafe. The proposed correction makes selected adapters inherit the relevant lifetime property from their underlying view.

This is a concrete instance of a broader difficulty with generic code. A type category can be conservative without accurately describing every composition of types. The library needs enough information to reject an iterator that depends on a destroyed object while retaining one whose required storage remains alive.

RAII is one of C++'s strongest arguments to me, but it also makes the end of each object's lifetime a definite event that other abstractions must respect. Ranges need to identify which object an iterator depends on. Merely observing that some temporary is destroyed gives an incomplete answer.

The wrapper and the storage are different objects

The paper's motivating example searches a string and a reversed view of that string. The string is supplied by reference. Reversal creates a temporary wrapper used during the search, while the characters belong to the original string.

The question is whether the returned iterator requires the wrapper to remain alive. If it points into the underlying string without depending on state in the wrapper, destroying that wrapper does not itself remove the required storage. The caller still needs the string to remain valid.

That last requirement matters. A safe wrapper classification does not extend the lifetime of the referenced string. It only says that the wrapper's own lifetime is not the dependency that invalidates the iterator. An application can still destroy the actual owner too early.

The distinction gives the safety property a precise scope. It addresses independence from one object, not freedom from every possible lifetime problem. A name such as safe range needs that explanation to avoid suggesting a stronger guarantee.

A result type can refuse to provide an iterator

The contemporary design described in P2017R0 uses safe_iterator_t and dangling to control whether a range algorithm returns a usable iterator. When the range fails the required property, the algorithm can still perform its search without exposing an iterator whose validity the library cannot justify.

This is a useful design because the danger often occurs after the algorithm returns. The search can be meaningful during the argument's lifetime. Its result becomes problematic when the caller tries to retain a reference into something that no longer exists.

The proposed change narrows an overly conservative classification. It does not require the algorithm to infer ownership at runtime. The relevant relationship is encoded in the adapter's type and the underlying view's stated property.

That makes composition possible without adding a separate dynamic lifetime check to each search. A library implementation can determine the permitted result type from the participating types. The application then receives either the useful iterator type or a result that prevents treating it as one.

Some adapters need their parent state

A reversal adapter can propagate an iterator without storing a predicate that the iterator needs later. A filtering adapter has a different dependency. Its iteration behavior can require the predicate held by the parent view.

P2017R0 distinguishes these cases and proposes conditional treatment for selected adapters, including reversal and taking or dropping elements. It leaves adapters requiring parent-owned state outside that change. The earlier P1870R1 explains the underlying opt-in mechanism and why expressing this property through subtle overload requirements was difficult.

The practical lesson for a view author is to inspect iterator operations, not merely the view's ownership of elements. An iterator can need parent state even when the parent owns none of the elements. Incrementing or dereferencing may use a callable or other information stored in that parent.

Consequently, non-owning is an insufficient synonym for independent lifetime. The proof needs to account for every object the iterator uses. An adapter that adds a new dependency cannot simply inherit the underlying range's guarantee.

Conditional properties preserve useful composition

The proposed treatment lets a wrapper preserve a property that its implementation does not invalidate. A sequence of suitable adapters can then retain the property through composition. That matches the structure of the program more accurately than a fixed list of independently safe leaf types.

For application code, the gain is less artificial naming of temporary wrappers merely to satisfy an overly strict classification. A named wrapper can extend its lifetime, but that does not explain why the lifetime was necessary. Where the iterator is already independent, the additional object obscures the actual dependency.

I find this a useful direction for C++ abstractions. The implementation can retain type-based checking while accepting a composition whose lifetime argument is sound. The resulting contract still requires the application to preserve the actual underlying storage for as long as it uses the returned iterator.