June 16, 2023·4 min read·engineeringcppstandardscallbackslifetime

C++26 starts with a non-owning callable interface

Varna adopts the first C++26 papers, including function_ref, which gives callback parameters reference semantics without making the receiving function a template.

Accepting a callback does not always require owning it. A synchronous algorithm may only need to invoke an existing callable while its caller remains on the stack. C++26's first adopted features include a vocabulary type for that relationship, function_ref.

Herb Sutter's June 16 Varna report describes adoption of forty change papers into the new C++26 working draft. Among them is P0792R14, by Vittorio Romeo, Zhihao Yuan, and Jarrad Waterloo. The facility supplies reference semantics for callable entities with a stated signature.

I like this distinction because ownership should be an explicit part of an interface. C++ already has several ways to accept work supplied by a caller. A named non-owning callable type can describe a narrower requirement without making each receiving function's entire implementation a template.

Existing choices impose different requirements

The proposal compares function pointers, templates, and owning call wrappers. A function pointer has a compact interface with a fixed signature, but cannot by itself carry the state of a capturing lambda. A function template can accept that lambda directly, at the cost of making its implementation available for instantiation.

An owning wrapper supplies another tradeoff. It can keep a callable as part of its own state, which is appropriate when an operation needs to retain the callback. That introduces construction requirements and storage machinery that a synchronous borrowed call may not need.

The proposal's function_ref expresses the borrowed case. Its receiver knows the call signature while the concrete callable type is erased. Ownership remains elsewhere, and the reference does not acquire the responsibility to destroy it.

That is a semantic choice before it is a performance choice. A callback saved for later needs a lifetime arrangement that survives the current call. A callback invoked only during the receiving operation may have a simpler requirement. Using different types helps keep those cases distinguishable.

The paper motivates this with a synchronous retry operation. The operation repeatedly invokes a supplied action and returns when the action succeeds or the attempt limit is reached. It needs the action during those attempts, without necessarily needing to own a copy after it returns.

Type erasure uses an invocation function

P0792R14 describes a representation containing a bound entity and a function pointer used to invoke it. The bound entity can represent an object or function pointer. The invocation function adapts that representation to the requested signature.

This keeps the receiving function independent of the callable's concrete type. It calls through the stored invocation mechanism with the arguments required by the signature. The callable's state remains where its owner placed it.

The paper specifies the specialization as trivially copyable. Copying the reference wrapper does not copy the underlying callable object. That relationship resembles other reference-like views, whose cheap copying does not establish independent ownership of the data they describe.

A conceptual interface under the adopted draft facility is short:

#include <functional>

int invoke_once(std::function_ref<int()> action)
{
    return action();
}

This is draft C++26 syntax and library usage. It is not a claim that a current C++23 standard library provides the type. The example illustrates the signature of a function borrowing an action for a synchronous invocation.

The receiver still has to honor that borrowed relationship. Storing the reference beyond the callable's lifetime would violate the intended use. A compact wrapper does not make a temporary callable persist after its ordinary lifetime ends.

Function-pointer lifetime required a design decision

The proposal's history examines how construction from a function pointer should work. A wrapper that merely points at a temporary function-pointer object can dangle even though the function itself continues to exist. That is an unnecessary difference between passing a function and passing its address.

The adopted design stores a function pointer value in that case. This removes the intermediate pointer object's lifetime from the relationship. The function reference can identify the function without borrowing a temporary variable that happened to hold its address.

Pointer-to-member support raises another representation issue. The paper avoids treating every runtime pointer-to-member like an ordinary callable object and instead includes facilities using a non-type template argument tag. The design balances supported forms with representation and lifetime constraints.

These details explain why a non-owning wrapper is not merely a stripped-down owning wrapper. It needs constructors that preserve a valid referent relationship. Eliminating allocation alone would not settle those rules.

For me, the function-pointer case is the most useful design example in the paper. A borrowed interface should not introduce an avoidable dangling relationship around an entity whose lifetime was otherwise sufficient. The representation has to match the thing being borrowed.

The interface also has no ordinary empty state or default constructor in the proposal. That choice keeps the vocabulary focused on an available callable, rather than making every invocation account for an absent target. Optionality can be represented separately when an API needs it. This is another way the reference semantics shape the public contract, since a receiver can require an action without also choosing an owning storage model for that action.

Adoption and availability remain different stages

Varna begins loading features into C++26 after C++23's technical work closed. Sutter explains that subgroup development continues while an earlier edition goes through completion, so many proposals were ready when the next working draft opened.

The forty papers include bundled issue resolutions as well as individual features. Counting papers therefore does not count forty facilities of equal size. The practical question is which semantic changes have entered the draft.

The meeting also adopts hazard pointers and RCU facilities for deferred reclamation. Those address a different lifetime problem, determining when shared objects can be reclaimed while other threads may still access them. Their presence beside function_ref shows several distinct ownership and lifetime concerns entering the new cycle.

A non-owning callable interface has a narrower purpose. It makes the callback signature available without retaining the callback object. C++26 adoption establishes that direction in the working draft, while implementations will determine when programmers can use the facility in their selected toolchains.