Skip to content

Commit ba15593

Browse files
committed
fix(core): Func<void> and Converter<T, void> are ill-formed (#2299, SR-AUD-126)
Func<void> compiled, and it was THE SAME TYPE as Action -- not convertible to it, the same type, because an alias template introduces no new type. Converter<T, void> collapsed onto ActionT<T> the same way. .NET cannot express any of this: void is not a permitted C# generic argument. Landed under SA-8 rather than SA-10: nothing here changes a signature, the port was simply MORE PERMISSIVE than .NET, which is exactly SA-8's direction. All 17 Func/FuncT* aliases and BOTH Converter declarations are constrained. Both mattered: System/Converter.hpp and System/Action.hpp declare it identically, and a repair touching one would have left the other accepting void. THE FINDING'S SECOND PRESCRIPTION IS STRUCTURALLY IMPOSSIBLE AND IS NOT ATTEMPTED, which the review had already established and which is now recorded as a declaration rather than left as a gap: with alias templates there is only ONE type, so no declaration can accept an Action and reject a Func-shaped callable. Constraining removes the SPELLING; it cannot create a CATEGORY. Preserving the category would mean replacing every alias with a distinct class type -- a whole-API break this repository has not asked for. A DETECTION-IDIOM DETAIL WORTH KEEPING, measured while writing the fixture: testing for the absence of a constrained alias must be written over a DEPENDENT parameter. gcc evaluates a constrained alias eagerly in a non-dependent 'requires', so the direct spelling is a HARD ERROR rather than a false value -- and in the fixture it leaked a diagnostic outside its own site region, making every verdict in that file untrustworthy until it was rewritten. Fixture set: 32 fixtures / 184 sites. Gate 17,283 run, 0 failed. Module graph unchanged at 41/92.
1 parent 14f964b commit ba15593

9 files changed

Lines changed: 269 additions & 21 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

audit/AUDIT_FINDINGS_INDEX.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<!-- SPDX-License-Identifier: MIT -->
2+
<!-- Copyright (c) Robert Vokac and contributors -->
3+
4+
# Migration — `Func<void>` and `Converter<T, void>` no longer exist (ticket #2299)
5+
6+
*2026-08-18.* `Func<void>` compiled, and it was **the same type as `Action`** — not convertible to
7+
it, the same type, because an alias template introduces no new type. .NET cannot express any of
8+
this: `void` is not a permitted C# generic argument.
9+
10+
Landed under `docs/StandingApprovals.md` **SA-8** (the port was *more permissive* than .NET) with
11+
SA-2's five conditions. A **compile-domain** source break: no runtime behaviour changes.
12+
13+
---
14+
15+
## 1. What changed
16+
17+
| Spelling | Was | Is |
18+
|---|---|---|
19+
| `Func<void>` | compiled — **and was `Action`** | **ill-formed** |
20+
| `FuncT<T, void>` … all 17 arities | compiled | **ill-formed** |
21+
| `Converter<T, void>` | compiled — **and was `ActionT<T>`** | **ill-formed** |
22+
| `Func<int>`, `Converter<int, std::string>`, … || **unchanged** |
23+
| `Action`, `ActionT<T>` || **unchanged** |
24+
25+
Both `Converter` declarations moved — `System/Converter.hpp` and `System/Action.hpp` declare it
26+
identically, and a repair that touched one would have left the other accepting `void`.
27+
28+
## 2. What this does NOT do, and cannot
29+
30+
The finding's second prescription — *"preventing APIs that require .NET parity from accepting the
31+
substitute aliases"* — is **structurally impossible** with alias templates. There is only **one
32+
type**, so no declaration can accept an `Action` and reject a `Func`-shaped callable.
33+
34+
Constraining removes the **spelling**; it cannot create a **category**. Preserving the category
35+
would mean replacing every alias with a distinct class type — a whole-API break this repository
36+
has not asked for. A test records that limit as a declaration rather than leaving it to be
37+
rediscovered as a gap.
38+
39+
## 3. To migrate
40+
41+
```cpp
42+
Func<void> f; // now ill-formed
43+
Action f; // what you meant, and it always existed
44+
45+
Converter<int, void> h; // now ill-formed
46+
ActionT<int> h; // what you meant
47+
```
48+
49+
## 4. A detection-idiom detail worth keeping
50+
51+
Testing for the absence of a constrained alias must be written over a **dependent** parameter:
52+
53+
```cpp
54+
template <typename R> concept FuncIsSpellable = requires { typename System::Func<R>; };
55+
static_assert(!FuncIsSpellable<void>); // works
56+
57+
static_assert(!requires { typename System::Func<void>; }); // HARD ERROR on gcc
58+
```
59+
60+
gcc evaluates a constrained alias eagerly in a **non-dependent** `requires`, so the direct form is
61+
an error rather than a false value. Measured while writing the negative fixture, where it leaked a
62+
diagnostic outside its own site region and made every verdict in that file untrustworthy until it
63+
was rewritten.
64+
65+
## 5. Downstream, measured
66+
67+
Neither `cna` nor `mobile-eggbert` names any `Func`/`FuncT`/`Converter` alias at all — **zero
68+
sites in both** — and neither spells `Func<void>` or `Converter<T, void>` anywhere. Zero
69+
production sites in this repository name them either.

modules/core/include/System/Action.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#pragma once
55

66
#include <functional>
7+
#include "System/Func.hpp"
78
#include "System/Span.hpp"
89

910
namespace System {
@@ -185,7 +186,7 @@ namespace System {
185186
* SR-AUD-126 note: `TOutput` is unconstrained, so `Converter<T, void>` is
186187
* the same type as `ActionT<T>` here and is not expressible in .NET at all.
187188
*/
188-
template<typename TInput, typename TOutput>
189+
template<typename TInput, NonVoidResult TOutput>
189190
using Converter = std::function<TOutput(TInput)>;
190191

191192
} // namespace System

modules/core/include/System/Converter.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
44
#pragma once
55
#include <functional>
6+
#include "System/Func.hpp"
67

78
namespace System {
89

@@ -24,7 +25,7 @@ namespace System {
2425
* is a compile-domain public source break either way. This alias is declared
2526
* identically in `System/Action.hpp`; both spellings mean the same type.
2627
*/
27-
template<typename TInput, typename TOutput>
28+
template<typename TInput, NonVoidResult TOutput>
2829
using Converter = std::function<TOutput(TInput)>;
2930

3031
} // namespace System

modules/core/include/System/Func.hpp

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#pragma once
55

66
#include <functional>
7+
#include <type_traits>
78

89
namespace System {
910

@@ -17,7 +18,20 @@ namespace System {
1718
* `FuncT<T, R>`, `FuncT2<T1, T2, R>` … `FuncT16<…>`. Ported code changes
1819
* spelling with arity, and the result type is always **last**, as in .NET.
1920
*
20-
* @warning **`R` is unconstrained, so `Func<void>` compiles and is the very
21+
* @par `R` is constrained to a non-void type since ticket #2299
22+
* `Func<void>` used to compile, and it was **the same type as** `Action` — not convertible
23+
* to it, the same type, because an alias template introduces no new type. `Converter<T,
24+
* void>` and `ActionT<T>` coincided the same way. .NET cannot express any of this: `void` is
25+
* not a permitted C# generic argument, so `Func<void>` does not exist there and the port was
26+
* merging two delegate categories under incompatible public names.
27+
*
28+
* `NonVoidResult` makes the spelling ill-formed, which is as far as an alias can go. **The
29+
* finding's second prescription is structurally impossible and is not attempted**: with
30+
* aliases there is only ONE type, so no declaration can accept `Action` and reject
31+
* `Func<void>`. Preserving the category would mean replacing every alias with a distinct
32+
* class type — a whole-API break this repository has not asked for.
33+
*
34+
* @warning **Historical note.** Before #2299, `R` was unconstrained, so `Func<void>` was the very
2135
* same type as `Action`** — not merely convertible to it, the same type,
2236
* because both are aliases of `std::function<void()>`. `Converter<T, void>`
2337
* and `ActionT<T>` coincide the same way. .NET keeps the two categories
@@ -44,47 +58,57 @@ namespace System {
4458
* it throws `std::bad_function_call`, which is `std::function`'s own
4559
* diagnostic and not a `System::Exception`.
4660
*/
61+
/**
62+
* @brief The constraint every `Func`/`FuncT*` result type must satisfy (ticket #2299).
63+
*
64+
* `void` is not a permitted generic argument in C#, so .NET has no `Func<void>`. Spelling one
65+
* here produced `Action` — the same type, not merely a convertible one — which merged two
66+
* delegate categories under incompatible public names.
67+
*/
4768
template<typename R>
69+
concept NonVoidResult = !std::is_void_v<R>;
70+
71+
template<NonVoidResult R>
4872
using Func = std::function<R()>;
4973

5074
/**
5175
* @brief Encapsulates a method that has one parameter and returns a value.
5276
*
5377
* C++ counterpart of the .NET System.Func<T, TResult> delegate.
5478
*/
55-
template<typename T, typename R>
79+
template<typename T, NonVoidResult R>
5680
using FuncT = std::function<R(T)>;
5781

5882
/**
5983
* @brief Encapsulates a method that has two parameters and returns a value.
6084
*
6185
* C++ counterpart of the .NET System.Func<T1, T2, TResult> delegate.
6286
*/
63-
template<typename T1, typename T2, typename R>
87+
template<typename T1, typename T2, NonVoidResult R>
6488
using FuncT2 = std::function<R(T1, T2)>;
6589

6690
/**
6791
* @brief Encapsulates a method that has three parameters and returns a value.
6892
*
6993
* C++ counterpart of the .NET System.Func<T1, T2, T3, TResult> delegate.
7094
*/
71-
template<typename T1, typename T2, typename T3, typename R>
95+
template<typename T1, typename T2, typename T3, NonVoidResult R>
7296
using FuncT3 = std::function<R(T1, T2, T3)>;
7397

7498
/**
7599
* @brief Encapsulates a method that has four parameters and returns a value.
76100
*
77101
* C++ counterpart of the .NET System.Func<T1, T2, T3, T4, TResult> delegate.
78102
*/
79-
template<typename T1, typename T2, typename T3, typename T4, typename R>
103+
template<typename T1, typename T2, typename T3, typename T4, NonVoidResult R>
80104
using FuncT4 = std::function<R(T1, T2, T3, T4)>;
81105

82106
/**
83107
* @brief Encapsulates a method that has five parameters and returns a value.
84108
*
85109
* C++ counterpart of the .NET System.Func<T1, T2, T3, T4, T5, TResult> delegate.
86110
*/
87-
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename R>
111+
template<typename T1, typename T2, typename T3, typename T4, typename T5, NonVoidResult R>
88112
using FuncT5 = std::function<R(T1, T2, T3, T4, T5)>;
89113

90114
/**
@@ -93,7 +117,7 @@ namespace System {
93117
* C++ counterpart of the .NET System.Func<T1..T6, TResult> delegate.
94118
*/
95119
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename T6,
96-
typename R>
120+
NonVoidResult R>
97121
using FuncT6 = std::function<R(T1, T2, T3, T4, T5, T6)>;
98122

99123
/**
@@ -102,7 +126,7 @@ namespace System {
102126
* C++ counterpart of the .NET System.Func<T1..T7, TResult> delegate.
103127
*/
104128
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename T6,
105-
typename T7, typename R>
129+
typename T7, NonVoidResult R>
106130
using FuncT7 = std::function<R(T1, T2, T3, T4, T5, T6, T7)>;
107131

108132
/**
@@ -111,7 +135,7 @@ namespace System {
111135
* C++ counterpart of the .NET System.Func<T1..T8, TResult> delegate.
112136
*/
113137
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename T6,
114-
typename T7, typename T8, typename R>
138+
typename T7, typename T8, NonVoidResult R>
115139
using FuncT8 = std::function<R(T1, T2, T3, T4, T5, T6, T7, T8)>;
116140

117141
/**
@@ -120,7 +144,7 @@ namespace System {
120144
* C++ counterpart of the .NET System.Func<T1..T9, TResult> delegate.
121145
*/
122146
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename T6,
123-
typename T7, typename T8, typename T9, typename R>
147+
typename T7, typename T8, typename T9, NonVoidResult R>
124148
using FuncT9 = std::function<R(T1, T2, T3, T4, T5, T6, T7, T8, T9)>;
125149

126150
/**
@@ -129,7 +153,7 @@ namespace System {
129153
* C++ counterpart of the .NET System.Func<T1..T10, TResult> delegate.
130154
*/
131155
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename T6,
132-
typename T7, typename T8, typename T9, typename T10, typename R>
156+
typename T7, typename T8, typename T9, typename T10, NonVoidResult R>
133157
using FuncT10 = std::function<R(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10)>;
134158

135159
/**
@@ -138,7 +162,7 @@ namespace System {
138162
* C++ counterpart of the .NET System.Func<T1..T11, TResult> delegate.
139163
*/
140164
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename T6,
141-
typename T7, typename T8, typename T9, typename T10, typename T11, typename R>
165+
typename T7, typename T8, typename T9, typename T10, typename T11, NonVoidResult R>
142166
using FuncT11 = std::function<R(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11)>;
143167

144168
/**
@@ -148,7 +172,7 @@ namespace System {
148172
*/
149173
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename T6,
150174
typename T7, typename T8, typename T9, typename T10, typename T11, typename T12,
151-
typename R>
175+
NonVoidResult R>
152176
using FuncT12 = std::function<R(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12)>;
153177

154178
/**
@@ -158,7 +182,7 @@ namespace System {
158182
*/
159183
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename T6,
160184
typename T7, typename T8, typename T9, typename T10, typename T11, typename T12,
161-
typename T13, typename R>
185+
typename T13, NonVoidResult R>
162186
using FuncT13 = std::function<R(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13)>;
163187

164188
/**
@@ -168,7 +192,7 @@ namespace System {
168192
*/
169193
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename T6,
170194
typename T7, typename T8, typename T9, typename T10, typename T11, typename T12,
171-
typename T13, typename T14, typename R>
195+
typename T13, typename T14, NonVoidResult R>
172196
using FuncT14 = std::function<R(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
173197
T14)>;
174198

@@ -179,7 +203,7 @@ namespace System {
179203
*/
180204
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename T6,
181205
typename T7, typename T8, typename T9, typename T10, typename T11, typename T12,
182-
typename T13, typename T14, typename T15, typename R>
206+
typename T13, typename T14, typename T15, NonVoidResult R>
183207
using FuncT15 = std::function<R(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
184208
T14, T15)>;
185209

@@ -190,7 +214,7 @@ namespace System {
190214
*/
191215
template<typename T1, typename T2, typename T3, typename T4, typename T5, typename T6,
192216
typename T7, typename T8, typename T9, typename T10, typename T11, typename T12,
193-
typename T13, typename T14, typename T15, typename T16, typename R>
217+
typename T13, typename T14, typename T15, typename T16, NonVoidResult R>
194218
using FuncT16 = std::function<R(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
195219
T14, T15, T16)>;
196220

modules/core/tests/System/FuncTests.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Copyright (c) Robert Vokac and contributors
33
// Portions based on .NET runtime API (MIT License, Copyright .NET Foundation and Contributors)
44
#include <gtest/gtest.h>
5+
#include "System/Action.hpp"
6+
#include "System/Converter.hpp"
7+
#include <type_traits>
58
#include <functional>
69
#include <stdexcept>
710
#include <string>
@@ -146,3 +149,54 @@ TEST(FuncTests, ReferenceResultIsNotCopied) {
146149
EXPECT_EQ(storage, 11);
147150
EXPECT_EQ(&byReference(), &storage);
148151
}
152+
153+
// ---------------------------------------------------------------------------
154+
// #2299 / SR-AUD-126 — Func<void> and Converter<T, void> no longer exist
155+
// ---------------------------------------------------------------------------
156+
157+
namespace detail2299 {
158+
/// Detection idiom over the alias templates: an ill-formed spelling must be expressible as a
159+
/// value rather than as a hard error.
160+
template <typename R>
161+
concept FuncIsSpellable = requires { typename System::Func<R>; };
162+
template <typename T, typename R>
163+
concept ConverterIsSpellable = requires { typename System::Converter<T, R>; };
164+
} // namespace detail2299
165+
166+
TEST(FuncTests, Fix2299_FuncOfVoidIsIllFormed) {
167+
// THE FINDING. `Func<void>` used to compile, and it was THE SAME TYPE as Action -- not
168+
// convertible to it, the same type, because an alias template introduces no new type. .NET
169+
// cannot express this at all: `void` is not a permitted C# generic argument, so `Func<void>`
170+
// does not exist there, and the port was merging two delegate categories under incompatible
171+
// public names.
172+
static_assert(!detail2299::FuncIsSpellable<void>, "#2299: Func<void> is ill-formed");
173+
static_assert(detail2299::FuncIsSpellable<int>, "...and every ordinary result type still works");
174+
static_assert(detail2299::FuncIsSpellable<std::string>, "including a non-trivial one");
175+
176+
static_assert(!detail2299::ConverterIsSpellable<int, void>,
177+
"#2299: Converter<T, void> is ill-formed too -- it collapsed onto ActionT<T>");
178+
static_assert(detail2299::ConverterIsSpellable<int, std::string>,
179+
"...and an ordinary conversion still works");
180+
}
181+
182+
TEST(FuncTests, Decl2299_TheCATEGORIESStillCannotBeSeparated) {
183+
// WHAT #2299 DID NOT DO, AND CANNOT. The finding's second prescription -- "prevent APIs that
184+
// require .NET parity from accepting the substitute aliases" -- is STRUCTURALLY IMPOSSIBLE
185+
// with alias templates: there is only one type, so no declaration can accept an Action and
186+
// reject a Func-shaped callable. Constraining removes the SPELLING; it cannot create a
187+
// category.
188+
//
189+
// Preserving the category would mean replacing every alias with a distinct class type, a
190+
// whole-API break this repository has not asked for. This row records that limit as a
191+
// declaration rather than leaving it to be rediscovered as a gap.
192+
static_assert(std::is_same_v<System::Action, std::function<void()>>,
193+
"Action is still a bare alias, so it has no identity of its own");
194+
static_assert(std::is_same_v<System::Func<int>, std::function<int()>>,
195+
"and so is Func -- the constraint is on the SPELLING, not on the type");
196+
197+
// An ordinary Action still works, unchanged.
198+
int calls = 0;
199+
System::Action a = [&calls] { ++calls; };
200+
a();
201+
EXPECT_EQ(1, calls);
202+
}

plan.sqlite3

4 KB
Binary file not shown.

0 commit comments

Comments
 (0)