Skip to content

[STF] exception_policy: an lvalue erased sink is viewed, not cloned, and an unhooked success passes unboxed - #12000

Closed
caugonnet wants to merge 21 commits into
NVIDIA:mainfrom
caugonnet:stf/exception-policy-erased-sink-fast-path
Closed

caugonnet wants to merge 21 commits into
NVIDIA:mainfrom
caugonnet:stf/exception-policy-erased-sink-fast-path

Conversation

@caugonnet

@caugonnet caugonnet commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Description

Stacked on #11967 (itself on #11889 and #11888; only the last commit here is new).

Behind exception_sink (type_erase), every guard cost about 18 ns per call on the success path, ten times the typed sink, and none of it was the virtual dispatch that erasure asks for. Two causes in the shell, both invisible to users of the API:

  1. The sink was cloned on every call. __normalize returned every sink-tagged policy by value, so the erased sink handed to on_throw was copied, and exception_sink's copy constructor clones the boxed policy: one heap allocation and one deallocation per guarded call, however the sink was passed. An lvalue exception_sink now normalizes to a by-reference view (__erased_ref_t) that forwards the hook, the success channel and the entry gate. An rvalue is still moved in, so a sink built on every call keeps its one allocation, which is what building it costs.
  2. The result was boxed for a hook that did not exist. The callable's result was put into std::any, passed through the virtual on_success(std::any) and unboxed with a type check, although the wrapped policy had no success hook. sink_base now records at erasure time whether the policy has one (has_success_hook, conservatively true for hand-written models), and the shell returns the value unboxed when it does not.

Behaviour is unchanged on every path. The remaining 2 to 3 ns over hand-written code is the virtual entry gate and the view.

Checklist

  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

🤖 Generated with Claude Code

caugonnet and others added 4 commits October 6, 2026 07:34
…alternations compile

A value-answering alternation (or repetition) used as a non-outermost
operand failed to compile:

    notify & (catch_only<E>(subst(1)) | subst(2))   // error
    a | b | subst(3)                                 // error, either association
    notify & catch_only<E>(subst(1)) | subst(2)      // fine: alternation outermost

The capability probes (__has_exception_hook, __hook_answer_t) call a hook
with the void(&)() archetype. __policy_or and __policy_pow returned
decltype(auto), so the probe had to instantiate the body under that
archetype, and the body's __interpret_answer<void> of a subst arm is a
hard static_assert rather than a substitution failure. The documented
forms (notify & retry * 3 | subst(-1)) never exposed this because
precedence puts the alternation outermost.

The answer type of these composites depends only on the callable: its
result, or the resume tag for a void callable. Spell it as the declared
return type (__composite_answer_t<_Fn>) so probes read the declaration
and the body is instantiated only at a real guard. No behavior change for
code that compiled before.

Adds a unit test covering the nested-under-&, three-arm (both
associations), repetition-under-&, alternation-of-sequences, and
reference-result cases, plus the outermost forms as a control.

Found while porting the paper's examples against the header; see the
Exception Sinks repository, cpp/NOTES.md. Verified with the
cudax.test.stf.unittest_headers.__stf.utility.exception_policy and
cudax.test.stf.utility.exception_policy_c_headers targets (nvcc 13.4,
gcc host, C++17); the failing shapes reproduce on main with a host-only
g++ build and compile and run correctly with this change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e composite dispatch

Two changes to `|` and `*`, measured against hand-written try/catch.

Declining cost two unwinds per declined arm: the hook's `throw;`, then the
composite's catch-all re-threw the exception to recover its std::exception
pointer in a nested try. Typed catch clauses on the composite's own try
recover the pointer directly, so a declined exception is caught once.

Composite guards cost 1 to 5 ns per call on the path where nothing throws,
against 2 ns for the call itself. GCC 13 and Clang 18 both left
__policy_or::operator() and __interpret_answer outlined, and one outlined
function on the failure path forces the policy aggregate, the source
location and the callable's captured references into memory before the
call. Marking the composite hooks and the answer interpreter
_CCCL_FORCEINLINE, with a lambda-free body for `|`, lets both compilers
keep everything in registers.

Measured (Neoverse-V2, -O2, median of Google Benchmark repetitions):
every composite guard now compiles to the same five instructions as the
hand-written try/catch and times the same (2.1 ns); a four-arm alternation
matching on its last arm falls from 10.3 to 6.2 us (one unwind per declined
arm instead of two). The header's 22 embedded unit tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A guard, translator or predicate on the left of `|` (or under `*`) used to decline by
`throw;`, one unwind per declined arm, so an n-arm typed ladder cost up to n throws where a
hand-written catch ladder costs one. This adds a claim protocol: such policies expose
`__claims(const std::exception*)` answering yes / no / unknown from the funnel pointer
(dynamic_cast or typeid, no rethrow), and `__claimed(e, loc, fn)` to run the hook body after a
yes so the selection is made once and a predicate is evaluated once. `|` asks its left arm
before invoking it: a no sends the exception to the right arm by return; a yes runs the arm
past its selection; unknown (non-std exception, non-class listed type) takes the previous
path, where the arm re-observes and declines by rethrowing. `*` does the same per repetition,
and a pure (type-based) no declines the whole repetition at once. `&` carries its head's
claim; `|` and `*` carry a claim when their parts' claims are pure.

Behaviour is unchanged on every path; what changes is the cost of a declined selecting arm.
Measured with the paper's harness (benchmarks/overhead, g++ 13.3 -O2, x86, 7 repetitions,
median ns): taken path, library vs hand-written

  2 arms + fallback, match on 2nd   2310 -> 1340   (hand 1255)
  4 arms + fallback, match on 4th   5129 -> 1424   (hand 1398)
  when(pred) false, then fallback   2283 -> 1269   (hand 1281)

Single-rule and one-arm cases unchanged; not-taken path unchanged within noise (3.3-4.0 ns,
same as hand-written). Handler code grows (4-arm alternation 1053 -> 3140 bytes) since each
level inlines both the claimed and the general arm body; it is on the failure path only.

Adds a UNITTEST pinning the claims, the fallbacks to re-observation, single evaluation of
predicates, an effect before a declining guard still running, and repetition over selectors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…and an unhooked success passes unboxed

Behind exception_sink every guard cost about 18 ns per call on the success path,
ten times the typed sink, and none of it was the virtual dispatch erasure asks
for. Two causes in the shell, both invisible to users of the API:

1. __normalize returned every sink-tagged policy by value, so the erased sink
   handed to on_throw was copied, and exception_sink's copy constructor clones
   the boxed policy: one heap allocation and one deallocation per guarded call,
   however the sink was passed. An lvalue exception_sink now normalizes to a
   by-reference view (__erased_ref_t) that forwards the hook, the success
   channel and the entry gate. An rvalue is still moved in, so a sink built on
   every call keeps its one allocation, which is what building it costs.

2. The callable's result was boxed into std::any, passed through the virtual
   on_success(std::any) and unboxed with a type check, although the wrapped
   policy had no success hook. sink_base records at erasure time whether the
   policy has one (has_success_hook, conservatively true for hand-written
   models), and the shell returns the value unboxed when it does not.

Measured with the paper harness on a pinned Neoverse-V2 (g++ 13.3 -O2, paired
with the hand-written try/catch, 101 blocks): a stored erased sink drops from
18.2-20.7 ns to 4.8-5.0 ns per call (+2.6 to +2.9 ns over hand-written), clang
18.1 from 17.6-18.6 to 4.3-4.4 (+1.9 to +2.3). A sink built per call stays at
14.3-15.8 ns. Throwing path within +45 to +93 ns on 1.3-1.6 us. Typed sinks are
untouched: their assembly is byte-identical before and after.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@caugonnet
caugonnet requested a review from a team as a code owner October 14, 2026 08:22
@caugonnet
caugonnet requested a review from andralex October 14, 2026 08:22
@copy-pr-bot

copy-pr-bot Bot commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-project-automation github-project-automation Bot moved this to Todo in CCCL Oct 14, 2026
@cccl-authenticator-app cccl-authenticator-app Bot moved this from Todo to In Review in CCCL Oct 14, 2026
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: NVIDIA/cccl
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: 2f554f9
  • Layers: Repository YAML
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: e0012d63-2fa2-449f-9017-efe1c0803802
Commits

Reviewing files that changed from the base of the PR and between 0ab152d and 19721ac.

Files selected for processing: 1
  • cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh
Files skipped from review as they are similar to previous changes: 1
  • cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

📝 Summary

Summary by CodeRabbit

  • New Features
    • Exception-handling policies can accept or decline exceptions, helping alternative and repeated handlers select an applicable response.
    • Predicates run once during handler selection. If a predicate declines or throws, selection can continue to another handler or attempt.
  • Bug Fixes
    • Successful values now pass through policies without success hooks, and void success hooks are skipped.
    • Non-const lvalue exception sinks are held by reference during guarded calls, avoiding an unnecessary copy.

Exception policies add claim results and claimed hooks for selectors, alternations, and repetitions. Erased sinks reference non-const lvalue sinks and record whether success hooks are available. Tests cover claim routing, predicate behavior, repetition, and callable paths.

Changes: Exception policy claims and erased sinks

Layer / File(s) Summary
Claim protocol and selector policies
cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh
Policies add claim outcomes and claimed hooks. Predicates, translators, and type guards classify exceptions for policy composition.
Composite routing and validation
cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh
Sequences, alternations, and repetitions route exception handling using claims. Tests cover fallback routing, predicate evaluation and exceptions, repetition behavior, and void-callable paths.
Erased sink reference and success dispatch
cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh
Guard normalization references non-const lvalue sinks. Erased models record success-hook availability and skip absent hooks.

Possibly related PRs

  • NVIDIA/cccl#11967: Adds claim-aware selection and routing for exception policies.
  • NVIDIA/cccl#11820: Introduces the errsink and exception_sink contracts extended here.
  • NVIDIA/cccl#11889: Changes composite exception dispatch that this change extends with claims.

Priority: Low

Change: Refactor

Merge Risk: Moderate · up to 19721

A predicate result that throws when converted to bool can terminate the process instead of reaching the next policy. The nvcc 12.0 repetition build concern also remains unresolved. Address or verify these before merging.

- [ ] Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository: NVIDIA/cccl/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: 8a26a3e3-911d-4540-b289-b13d3ccc437e
📥 Commits

Reviewing files that changed from the base of the PR and between 357705e and acf6fe2.

📒 Files selected for processing: 1
  • cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

caugonnet and others added 7 commits October 14, 2026 10:41
… and only for non-const lvalues

Review on NVIDIA#12000: the by-reference view sat in __normalize, which also serves
&, |, *, when and type_erase, so `s & notify` or `type_erase(s)` on a non-const
lvalue stored a reference to s instead of an owning clone (dangling once s is
gone, shared state across clones), and a const exception_sink lvalue did not
compile, since the view holds a non-const reference and the sink's hooks are
non-const. The view now lives in __normalize_for_guard, used only by on_throw,
whose policy lives for the guarded call's full expression, and only for
non-const lvalues; every other path, and every const sink, copies as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Takes main's hoisted third_try lambda and _CCCL_HAS_EXCEPTIONS() guard in the
nested-alternation test (fixes clang-tidy bugprone-inc-dec-in-conditions) and
keeps this branch's _CCCL_FORCEINLINE on the composite operator().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in error sinks A (NVIDIA#11820). The repetition comment keeps this branch's
"caught once" wording with A's fwd name. The nested-alternation test takes
the errsink port from NVIDIA#12224, which main needs as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…atus store

Same change as NVIDIA#12224: clang-tidy's cccl-prefer-cuda-traits rejects the
cuda::std spelling, which main carries since NVIDIA#11820.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`|` and `*` ask the left arm's claim before their try, so a `when` predicate that
throws escaped the whole alternation instead of reaching the next arm. A hook
that throws is the forward; before the claim protocol the predicate ran inside
the hook and `|` handed its exception to the right arm. A claim that may throw
(only a predicate's can) is now asked under its own try: `|` gives the right
arm the predicate's exception, and `*` declines that repetition with it.
Type-based claims are noexcept, propagated through the composites, so typed
ladders keep the plain path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olicy-typed-claim-fast-path

The claim members sit beside A's code-channel `handle` members in `&`, `|` and
`*` (`|`'s public `handle` ahead of its private helpers). Comments take A's
vocabulary (fwd, forward). The selecting-arms test moves to `eh` and
`errsink(...)->*f`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…olicy-erased-sink-fast-path

errsink's factory keeps this branch's __normalize_for_guard under A's eh namespace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@andralex

Copy link
Copy Markdown
Contributor

/ok to test 5bf1d82

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: NVIDIA/cccl
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: 72a288d
  • Layers: Repository YAML
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: 1c089bdc-629f-423d-913a-8169c327ce42
📥 Commits

Reviewing files that changed from the base of the PR and between d9b0b58 and 5bf1d82.

📒 Files selected for processing: 1
  • cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 8 remain after this review.

andralex and others added 2 commits October 23, 2026 17:24
…compile

nvcc 12.0 instantiates discarded constexpr branches inside generic lambdas, so
`retry * n` failed with "retry_t has no member __claims" in the repetition's
loop. The claim is now asked through `__ask_claim`, which answers `__unknown`
for a policy without one, and `__nothrow_claim` is defined for every policy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@andralex

Copy link
Copy Markdown
Contributor

/ok to test 3aa4a27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟠 Major · 🩺 Stability & Availability · exception_policy.cuh:642-645

cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh:642-645
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

important: Include the conversion to bool in __nothrow_pred. If a noexcept predicate returns a result whose operator bool() throws, __claims is marked noexcept and calls std::terminate. The next | arm cannot handle that exception. Test the full condition in the noexcept calculation, including its conversion to bool.

🧹 Nitpick comments: 2
cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh: 2

1844-1849: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

suggestion: Document the parameter and return value of __normalize_for_guard.

The //! @brief block for __normalize_for_guard has no //! @param for __r and no //! @return. The project guidance requires both for documented functions. Add them:

📝 Proposed documentation
 //! `type_erase`) keep copying, so a composite or an erased wrapper never aliases its operand.
+//! @param[in] __r The policy to normalize. It is not modified.
+//! @return A `__erased_ref_t` view of `__r` for a non-const lvalue erased sink. Otherwise, the result of `__normalize` on `__r`.
 template <class _R>
 auto __normalize_for_guard(_R&& __r)

3512-3512: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

suggestion: Track value and void success hooks separately.

For a policy with only on_success(), the non-void path does not need to invoke a hook. It currently still boxes the result, calls the identity on_success(std::any) virtual path, and unboxes it because the combined flag is true. Store separate value and void flags, and guard each exception_sink::on_success overload with its corresponding flag.


ℹ️ Review info
⚙️ Run configuration
  • Source mode: Auto
  • Repository: NVIDIA/cccl
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: 72a288d
  • Layers: Repository YAML
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: 9d33bd40-3541-434a-be5c-e31b63ffecca
📥 Commits

Reviewing files that changed from the base of the PR and between 5bf1d82 and 3aa4a27.

📒 Files selected for processing: 1
  • cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 7 remain after this review.

@github-actions

This comment has been minimized.

andralex and others added 2 commits October 26, 2026 10:31
The flag is written only in the constexpr branch taken when the arm has a
claim, so for other arms clang-tidy sees it as never modified.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@andralex

Copy link
Copy Markdown
Contributor

/ok to test 1f70e28

andralex and others added 4 commits October 26, 2026 10:31
…-typed-claim-fast-path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The squash commit's tree equals NVIDIA#11889's head merged into its parent, and this
branch already contains both, so the tree is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xception-policy-erased-sink-fast-path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@andralex

Copy link
Copy Markdown
Contributor

/ok to test 0ab152d

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


Review info
Run configuration
  • Source mode: Auto
  • Repository: NVIDIA/cccl
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: 2f554f9
  • Layers: Repository YAML
  • Review profile: CHILL
  • Plan: Enterprise
  • Run ID: e901c4ff-64dc-4637-a4c0-06bd53e6f772
Commits

Reviewing files that changed from the base of the PR and between 3aa4a27 and 0ab152d.

Files selected for processing: 1
  • cudax/include/cuda/experimental/__stf/utility/exception_policy.cuh

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

@github-actions

This comment has been minimized.

andralex and others added 2 commits October 26, 2026 18:23
catch_only and catch_exactly exposed __claims even when their policy has
no exception hook, while their __claimed and operator() were constrained on
the hook. A composite then took the claimed path and called a __claimed that
does not exist, so catch_only<E>(noop) & subst(1) | subst(2) failed to
compile (it compiles on main). Constrain the claims like the hooks; the
composite falls back to the general path, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@andralex

Copy link
Copy Markdown
Contributor

/ok to test 19721ac

@github-actions

Copy link
Copy Markdown
Contributor

🥳 CI Workflow Results

🟩 Finished in 2h 08m: Pass: 100%/73 | Total: 1d 19h | Max: 1h 27m | Hits: 16%/256601

See results here.

andralex added a commit to andralex/cccl that referenced this pull request Oct 29, 2026
The left operand of a sink's operators, `__sink_at<Self>`, held the sink by
value. For `dynamic` that copy clones the erased model, so every guarded
call on a named erased sink allocated. The capture now holds
`__sink_storage_t<Self>`, which is the sink itself for every concrete
policy and `__dynamic_view` for `dynamic`: a non-const lvalue is viewed,
an rvalue is moved in (a pointer move), and a const lvalue is still copied,
since its hooks would otherwise run on a const object.

The friend operators name their first parameter through `__sink_at_for<Self, X>`
so that the capture type depends on `X`: defined inside the class, they
would otherwise instantiate `__sink_at<dynamic>` before its storage type
can be complete.

Ports the first half of NVIDIA#12000 (Cédric Augonnet) onto the sink-is-its-own-
carrier design of NVIDIA#12107.

Co-Authored-By: Cédric Augonnet <caugonnet@nvidia.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
andralex added a commit to andralex/cccl that referenced this pull request Oct 29, 2026
…e unboxed

`sink_base` records at erasure time whether the wrapped policy has a
success hook. When it has none, `dynamic::on_success` returns the
callable's value as it is instead of boxing it in `std::any`, calling the
model, and unboxing it. Hand-written models keep the conservative default
and still go through the box.

Ports the second half of NVIDIA#12000 (Cédric Augonnet) onto NVIDIA#12107.

Co-Authored-By: Cédric Augonnet <caugonnet@nvidia.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@andralex

Copy link
Copy Markdown
Contributor

Now that #12107 has landed, the two fixes here are ported onto its design in #12325 (same two changes, you are co-author on both commits). Once that one is in, this PR can be closed.

andralex added a commit that referenced this pull request Oct 29, 2026
…cess passes unboxed (#12325)

* [STF] errsink: a named dynamic is viewed by << and ->*, not cloned

The left operand of a sink's operators, `__sink_at<Self>`, held the sink by
value. For `dynamic` that copy clones the erased model, so every guarded
call on a named erased sink allocated. The capture now holds
`__sink_storage_t<Self>`, which is the sink itself for every concrete
policy and `__dynamic_view` for `dynamic`: a non-const lvalue is viewed,
an rvalue is moved in (a pointer move), and a const lvalue is still copied,
since its hooks would otherwise run on a const object.

The friend operators name their first parameter through `__sink_at_for<Self, X>`
so that the capture type depends on `X`: defined inside the class, they
would otherwise instantiate `__sink_at<dynamic>` before its storage type
can be complete.

Ports the first half of #12000 (Cédric Augonnet) onto the sink-is-its-own-
carrier design of #12107.

Co-Authored-By: Cédric Augonnet <caugonnet@nvidia.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [STF] errsink: a dynamic over a hookless policy passes a success value unboxed

`sink_base` records at erasure time whether the wrapped policy has a
success hook. When it has none, `dynamic::on_success` returns the
callable's value as it is instead of boxing it in `std::any`, calling the
model, and unboxing it. Hand-written models keep the conservative default
and still go through the box.

Ports the second half of #12000 (Cédric Augonnet) onto #12107.

Co-Authored-By: Cédric Augonnet <caugonnet@nvidia.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* [STF] errsink: test the boxed success hook with a named policy; document the viewed dynamic

The test's local `counting` class was not a policy under GCC: its raw
hook took `std::function&`, which the capability probe's archetype cannot
bind to, so normalization made it a `subst_t` and the success hook never
ran. The policy is now at namespace scope with the templated `on_thrown`
hook, and the test asserts the capability it relies on.

The `sink` overview says a named sink is copied into each expression; a
named non-const `dynamic` is viewed instead, so the paragraph says so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Cédric Augonnet <caugonnet@nvidia.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@andralex andralex closed this Oct 29, 2026
@github-project-automation github-project-automation Bot moved this from In Review to Done in CCCL Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants