Skip to content

fix(function_ref): do not mark an inline-stored callable as borrowed - #13

Merged
psiha merged 1 commit into
masterfrom
fix/inline-stored-callables-are-not-borrowed
Sep 10, 2026
Merged

psiha merged 1 commit into
masterfrom
fix/inline-stored-callables-are-not-borrowed

Conversation

@psiha

@psiha psiha commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Problem

function_ref's converting constructor is annotated [[clang::lifetimebound]],
which is right for the general case: the ref points at the caller's object and
that object has to outlive it.

But make_c_callback does not always point. A callable that is trivially
copyable and fits in the data word is copied into the ref, which then owns
everything it needs and refers to nothing. For those, the annotation describes a
borrow that does not exist — and clang enforces it:

// captures one pointer -> trivially copyable, fits the data word -> copied
return function_ref<void( int )>{ [ p ]( int const v ) { *p = v; } };
// error: returning address of local temporary object [-Wreturn-stack-address]

Under -Werror that is a hard rejection of a perfectly sound use. The
workarounds are all worse than the thing they avoid: hoist the callable into a
named object whose type you often cannot spell (it may come from a
type-dispatched if constexpr branch), give it static storage and lose the
per-instance data, or silence the diagnostic and lose it everywhere.

Fix

[[clang::lifetimebound]] cannot be applied conditionally, so split the
constructor in two and let the constraints partition on whether the target is
stored inline:

  • not stored inline — the ref points at the caller's object. Keeps
    lifetimebound; behaviour unchanged.
  • stored inline — copied into the ref, borrows nothing, so a temporary is
    safe and must not be diagnosed.

The predicate is exposed as stored_inline<F> so callers can static_assert
the property they rely on rather than assuming it.

Note the split is on storage, not value category: an lvalue of an
inline-storable type was never borrowed either, so both overloads accept both
value categories and only the borrowing one carries the annotation.

Tests

Added to test/function_ref_test.cpp:

  • a ref built from a temporary capturing one pointer stays valid after the
    full-expression and can be returned;
  • stored_inline classifies a one-pointer capture and a plain function pointer
    as copied, and a two-pointer capture as borrowing;
  • the borrowing overload still accepts an lvalue.

Verified separately that a two-pointer capture returned by value is still
diagnosed by -Wreturn-stack-address — the safety property is preserved exactly
where it applies. Full suite: 6/6 green.

A callable that is trivially copyable and fits the data word is COPIED into the
ref by `make_c_callback` — the ref then owns everything it needs and points at
nothing. The single converting constructor nevertheless carried
`[[clang::lifetimebound]]`, so building a ref from a temporary of that kind was
diagnosed as a dangling reference and, under `-Werror`, rejected outright:

    return function_ref<void( int )>{ [ p ]( int const v ) { *p = v; } };
    error: returning address of local temporary object [-Wreturn-stack-address]

The attribute cannot be applied conditionally, so split the constructor in two
and let the constraints partition on whether the target is stored inline:

  - not stored inline -> the ref points at the caller's object, which must
    outlive it. Keeps `lifetimebound`, unchanged behaviour.
  - stored inline -> copied, borrows nothing, so a temporary is fine.

The classification is exposed as `stored_inline<F>` so callers can assert the
property they are relying on. Note it is about storage, not value category: an
lvalue of an inline-storable type was never borrowed either.

Tests: a ref built from a temporary that captures one pointer stays valid after
the full-expression and is returnable; a two-pointer capture is still classified
as borrowing (and still diagnosed as such by the compiler when returned).
@psiha
psiha merged commit 01d8f6f into master Sep 10, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant