Safe C++ proposes checked borrowing inside an explicit subset
P3390R0 combines borrow checking with new library types and an explicit safe context. Its iterator and lock examples show why lifetime checking requires information that conventional C++ interfaces often omit.
A vector can own its storage correctly while an iterator into that storage becomes invalid. Automatic destruction does not establish that every borrowed access remains valid until its last use. P3390R0, dated 11 September, proposes Safe C++ as a language extension addressing that relationship. Sean Baxter and Christian Mazakas combine a restricted safe context with borrow checking and replacement library interfaces. It is a concrete design and implementation proposal, without being an adopted guarantee of standard C++.
RAII remains C++'s strongest argument for me. Associating release with an object's lifetime makes normal cleanup comprehensible. Safe C++ is interesting because it tries to connect borrowed access to those same lifetimes, giving the compiler more information about when an operation would invalidate a reference.
The iterator example identifies a specific conflict
The proposal opens with a loop over a safe vector that attempts to append elements during iteration. Its implementation rejects the append because it conflicts with an active borrow used by the iterator. The error identifies the operation requesting mutation and the earlier loan on which iteration depends. This is evidence about the prototype's handling of that example, rather than a claim that every possible migration problem has been solved.
The underlying issue is a relationship between operations. Beginning iteration establishes an access that remains needed later. Mutating the vector can invalidate that access. A compiler inspecting only whether each individual call has a plausible parameter type lacks the relationship needed to reject the combination. Borrow checking records it as part of the program analysis.
This makes the diagnostic more meaningful than a general warning about modifying a collection in a loop. The invalidating action and the later dependent use are the reasons for rejection. An implementation can use the actual control-flow and lifetime information to decide whether the conflict exists. The proposal describes analysis over a mid-level intermediate representation for that purpose.
For a library author, the consequence is that lifetime relationships must be representable in the interface. A checker cannot derive a robust guarantee merely from the name of a member function. The safe vector and its iterator need contracts that expose how access depends on the container. That requirement explains why a new library accompanies the language work.
Safe code has explicit restrictions
P3390R0 adds a safe function context and distinguishes it from unsafe operations. Pointer dereference and pointer arithmetic are among the operations it excludes from ordinary safe use. It also restricts operations involving legacy references and unions. New borrow and choice types provide alternatives for important uses of those mechanisms.
The intended safety claim therefore applies to a defined subset. Existing C++ source continues to compile under the extension without automatically gaining the new guarantee. A program opts into the additional rules. This preserves a migration path while making it necessary to identify which regions actually satisfy the safety model.
That distinction should be explicit in an adoption proposal. Compiling a legacy project with a compiler containing Safe C++ is not equivalent to rewriting its interfaces to use checked borrowing. The new guarantees depend on the types and operations used within the selected context. A project needs to account for the boundary between those contexts as part of its design.
The paper also distinguishes static restrictions from runtime checks. An operation that cannot be established as safe statically can require a checked failure at runtime. Bounds checks that panic on invalid subscripts are an example. A defined termination path has a different security meaning from permitting undefined behavior, although it remains an application availability concern.
Mutation becomes a visible request
The design uses explicit mutation syntax to select mutable borrowing in C++'s overload system. A shared access and an exclusive mutable access have different obligations. The syntax tells overload resolution which sort of access the caller is requesting. Borrow analysis then checks whether that request conflicts with other active uses.
This is more than a style rule about making writes conspicuous. The distinction supplies information that determines which operation is selected and which lifetime constraints apply. A mutable request cannot simply coexist with every outstanding shared access. The program has to arrange its work so that those conflicting uses do not overlap.
I regard that as a meaningful change in the programming model. It moves part of the reasoning about interference into a form the compiler can check. The cost is that some familiar C++ patterns need new expression and ownership arrangements. That tradeoff should be evaluated against the specific invalid accesses the new model prevents.
It also changes how an API designer thinks about convenience. Returning a broad reference can make a call easy to write while creating a long-lived obligation for another operation. A more constrained access pattern can reduce that obligation. The new model encourages interfaces to describe the relationship instead of asking every caller to maintain it informally.
The library is part of the lifetime model
The proposal uses a safe library in namespace std2. Its explanation identifies conventional iterator pairs and initializer-list ownership as obstacles to checking the intended relationships. The replacement types carry lifetime and ownership information that the prototype's analysis can use. They should not be described as ordinary standard-library types made safe by one compiler option.
This is important for incremental integration. A component accepting a conventional container reference may need an adapter or a redesigned interface before it can participate in the checked model. Such work is part of adopting the proposal, rather than incidental syntax cleanup. The boundary determines which implementation is responsible for preserving validity.
The prototype's mutex example makes that responsibility concrete. Access to protected data depends on a lock guard. Destroying the guard before the borrowed access is finished invalidates the loan, and the checker rejects the later use. The lock's automatic release is retained, but its relationship to access is now also represented.
That example explains why shared ownership alone is insufficient. Keeping the object allocated and keeping access synchronized are separate obligations. A reference-counted owner can establish one without establishing the other. A safety model for concurrent use must account for both the object's lifetime and the conditions under which its state may be accessed.
Unsafe boundaries retain an audit obligation
P3390R0 provides explicit escapes for operations the checker cannot establish as sound. Its unsafe blocks and unsafe qualification identify places where the programmer assumes responsibility. Those mechanisms are intended to make integration with existing code possible. They also delimit the part of the proof that remains manual.
An unsafe marker is therefore evidence of an obligation, not evidence that the obligation has been discharged. A wrapper exposing a safe operation needs to validate the requirements of any unsafe implementation it invokes. If it accepts an invalid size and passes it to an unchecked primitive, the marker cannot repair that design.
The useful architectural question is how much of the product's memory-sensitive work can remain inside checked interfaces. A small boundary with a precise ownership contract is easier to inspect than many scattered escapes. Counting unsafe tokens alone would still miss the complexity of the assumptions each one carries.
Relocation changes the source object's state
The proposed object model also distinguishes relocation from copying. Relocation transfers the object into a new value and marks the source uninitialized. Copying retains an initialized source. Explicit syntax selects the non-trivial operation, and initialization analysis rejects later use of an uninitialized source. This gives the checker another state transition to account for alongside borrowing. It is a change to the program model, so migration needs to consider uses after transfer instead of treating relocation as a simple alternate spelling for an existing move expression.
The proposal responds to a product-level requirement
The February ONCD announcement asks manufacturers to prevent memory-safety vulnerability classes through implementation choices. Safe C++ proposes one in-language route for discussing that requirement in an existing C++ ecosystem. Its argument depends on enforceable restrictions and replacement abstractions, with explicit integration costs.
The immediate contribution is a testable design question for the community. Can the checked subset express the required components, and can their interfaces preserve lifetime information across integration boundaries? The iterator and lock examples show the intended mechanism at a useful scale. They establish which conflicting accesses the prototype rejects while leaving adoption, coverage and remaining unsafe obligations to be assessed for each concrete component.