August 4, 2023·4 min read·engineeringcppstandardscontainersmemory-safety

Safer range access needs more than a bounds check

P2955R0 separates index validation from reference lifetime and proposes value-oriented access with explicit error policies.

A bounds check establishes that an element exists at the moment of access. It does not establish that a returned reference will remain usable after the container changes. Jarrad J. Waterloo's P2955R0, dated August 4, treats those as separate problems and proposes value-oriented range access to reduce both.

The paper addresses containers and views including vector, array, span, and string types. Its motivating distinction is between checking an index and returning a reference whose lifetime remains the caller's responsibility. The proposal remains exploratory work for the safety and security subgroup, not an adopted replacement for existing container interfaces.

I find that distinction useful because C++'s resource ownership rules do not automatically make every borrowed reference safe. A container can own its storage correctly while a caller retains an element reference beyond the storage's valid use. An access interface should make the required lifetime reasoning visible.

Checked access still returns a reference

The GCC 13.2 vector implementation supplies a concrete example. Its at member checks the index against the vector's size, throws for an invalid index, and otherwise returns the indexed reference.

That check answers a precise question. It does not convert the reference into a separate object, and it does not make the reference own the vector's storage. A caller using the result later still needs the container relationship to remain valid.

A value copy expresses a different requirement:

#include <vector>

int first_value(const std::vector<int>& values)
{
    return values.at(0);
}

The function returns an integer value. If the vector is empty, the checked access throws. If an element exists, the returned integer does not borrow that element's storage. The example uses an existing interface to illustrate the semantic distinction motivating the proposal.

That is not a universal prescription to copy every object. A value can be expensive to copy, and some types cannot be copied at all. The point is that a value result and a reference result provide different lifetime relationships even when both begin with a valid index.

The proposal separates getting from setting

C++ subscripting commonly supplies a reference that serves both reading and writing. P2955R0 instead proposes separately named getters and setters. A getter returns a value, while a setter performs the requested update without returning an element reference.

The names also encode failure policy. Proposed variants include throwing, returning a default or an optional, and terminating. Setter variants can ignore an out-of-range write or grow the destination to accommodate it. These are different application policies, not merely alternate spellings for one check.

A default-returning operation treats a missing element as a value-selection decision. An optional-returning operation makes absence part of the result. A terminating operation treats the invalid request as a condition under which execution should not continue. Choosing among them belongs in the interface contract.

The breadth of this family is a design cost. Each additional policy needs consistent naming and behavior across the supported containers. The proposal makes the alternatives explicit, but the standard-library design still has to assess how much of that family is appropriate.

For me, the value of the paper lies in making the choice explicit before an application accumulates local helpers with incompatible meanings. A function called safe access is insufficiently precise if one implementation throws and another silently supplies a default. The failure behavior needs a name and a specification.

Transformations address work on part of an element

Copying a whole element is not always the operation an application needs. The paper therefore includes transformation and visitation forms. These let an operation derive a value from an indexed element or perform a requested update without returning the underlying reference as the access result.

This attempts to preserve useful work that reference-returning interfaces make convenient. A caller may need only one property of a large object. Returning a derived value can avoid copying the whole object while still preventing the accessor itself from exporting an element reference.

The mechanism does not justify treating arbitrary callbacks as automatically safe. A callback can have its own effects and obligations. The useful claim is narrower, the accessor's result need not expose the container's element reference.

That distinction matters when discussing value semantics. A function's return type can avoid a borrowing relationship while its implementation still operates on the stored object. The operation should document both the result and the callback's role.

Batch operations can make clipping a deliberate policy

P2955R0 also proposes copying between ranges with crop and grow behaviors. Cropping omits source elements that would exceed the destination's available range. Growing changes a suitable destination container so it can hold the requested data.

These operations describe different outcomes for the same boundary condition. A clipped image-like operation may intentionally discard out-of-range output. A data-preserving copy may instead require expansion or an error. Treating both as bounds checking would hide the choice that affects the result.

The paper's batch approach can also centralize checking instead of requiring a separate public access operation for every element. That is a useful interface direction, though any concrete implementation still needs correct arithmetic for sizes and positions.

For a view, growing storage is a different proposition from growing an owning container. That reinforces the need to specify each operation against the capabilities of its destination, rather than promising every policy for every range type.

Contracts do not replace the result-type decision

The paper anticipates future contract work but argues for addressing access interfaces directly. A precondition can describe a valid index. It does not by itself change a returned reference into an independent value.

This gives the discussion two separate axes. One concerns what happens when the index is invalid. The other concerns what relationship the successful result has to the container. Improving the first axis leaves the second relevant.

P2955R0 proposes changing both through explicitly named operations. Its practical contribution is to make a checked reference and a checked value visibly different interface choices. That distinction remains useful even before any of its proposed member names become part of an implementation.