Skip to main content

What Comes to Mind

Stuff, and more stuff

Initialization Profile Probes

These are the probes that go with the review of P4222R3's examples: small programs that ask what the two implementations of the initialization profile do with shapes the paper does not show. The compilers and flags are the ones described there, -std=c++23 -pedantic-errors, with the profile turned on by the attribute at the top of the file.

Read more…

The Initialization Profile

1. About this document

Every code block in P4222R3 (Stroustrup, 2026-10-22, "An initialization profile") is reproduced below, compiled under the profile on the two implementations the paper cites as "tested nightly" [Impl1, Impl2], with what the paper says should happen beside what does. Where a block needed scaffolding to be a translation unit (a definition of X, a declaration of my_malloc, a fix to a typo) the addition is marked in a comment; lines added to probe a consequence the paper does not spell out are marked // added.

The whole set was rechecked on 2026-11-14: every listing recompiled and compared, and each claim in the prose that goes beyond a listing tried with a small program of its own. The listings all stood. Some of the prose did not, and where a passage changed it carries a bracketed note beginning "Revised 2026-11-14" that says what the first version said.

Compilers, as built on Compiler Explorer on the date above:

Clang
CE id clang_profiles_init, built from cppalliance/clang branch profiles-init at commit 4a2d2673 (2026-10-15), an llvm-project fork based on main of 2026-07-20. Flags -std=c++23 -pedantic-errors.
GCC
CE id gcontracts-p4324-trunk, built from villevoutilainen/gcc branch p4324 at commit 146e7ba1 (2026-10-24/25), based on trunk of 2026-07-18. Same flags. [Revised 2026-11-14: Compiler Explorer rebuilds this compiler from the branch, and by the recheck it was at commit a9bec5e9 (2026-11-13). All 36 examples give the same diagnostics on that build. The Clang build had not changed.]

The profile is enabled in each case by the file-scope [[profiles::enforce(std::init)]]; from P3589; -fprofiles-enforce=std::init works on both as well.

Each Compiler Explorer link opens the source on the left and, for each compiler, the compiler pane with its diagnostics pane directly beneath it. Line numbers in the diagnostics match the numbered source blocks.

The examples are grouped by how badly the paper and the implementations disagree, worst first: rejected by both compilers where the paper says OK; rejected by Clang where the paper says OK; accepted by Clang where the paper says error; GCC alone differing; and the examples where both compilers agree with the paper. Within a group the examples follow the paper's order unless noted. The summary table at the end lists all 36 in the paper's order.

Of 36 examples, the two implementations give the same verdicts on 20 (and nearly so on 1 more); on the other 15 they disagree with each other, in most cases on lines the paper annotates. [Revised 2026-11-14: the first version had 18, 2 and 16. In §5.3 Label and §5.4 classes without constructors GCC's extra error against the type turned out to be its report of the same line Clang rejects.] Nine of the paper's examples are rejected by both compilers on a line the paper marks OK; they are the first group below, led by the §4.7 Vector the paper calls "guaranteed free from initialization errors". In two of the nine (§4.9 uninitialized_fill, §5.5 arrays) the paper's own text contradicts the annotation.

Three recurring causes account for most of the differences:

  1. GCC does not know the standard library. malloc returns initialized memory to it (§4.3 malloc/calloc note, §4.7 Vector, §4.11 malloc/new, §5.3 exposing uninitialized), and so does new int (§4.9 unique_ptr/vector); a second construct_at or destroy_at of the same object goes unnoticed (§4.6 destroy_at); std::uninitialized_fill and free are unannotated and so reject marked arguments (§4.7 Vector). Clang knows these by builtin ID. [Revised 2026-11-14: the first version said construct_at and destroy_at were unmodelled on GCC, and listed the §4.9 example under malloc, which it does not call. construct_at is modelled: GCC accepts std::construct_at(&x, 10) on an [[uninit]] local and a read of x after it, where the same address passed to an unannotated function template is an error. What GCC does not do is notice the second one.]
  2. GCC implements [[ref_to_uninit]] on pointers and references only: not on class-type handles (§4.6 construct_at, Appendix settled forms), not on a read through a marked return value (§4.5 Slot), and not on a reference bound to a local (§4.3 f1/f2/g). Clang implements all of these. [Revised 2026-11-14: the first version said "not on return types". GCC does check where a marked returned pointer is stored (§4.3 malloc/calloc); it is a read through the returned pointer or reference that it lets by. The Appendix placement example was also cited for references to locals, and it has none.]
  3. The attribute vocabulary has forked: the paper's [[must_init]] is unknown to Clang, which implements [[now_init]]; GCC implements [[must_init]] but on the parameter rather than in the paper's function position (§4.2 must_init, Appendix settled forms). std::now_init ships in GCC's <utility> and nowhere in Clang's library (§1.4 now_init to a span).

2. Rejected by both implementations where the paper says OK

The paper marks a line OK, or presents the form as the sanctioned one, and both compilers reject it. GCC's branch is a quick proof of concept and its verdict alone settles little; when Clang, the fuller implementation, and GCC agree in rejecting, the example does not work as written. The Vector comes first because the paper's claim for it is the strongest. In the last two the paper's own text contradicts the annotation the compilers reject.

2.1. §4.7 — The Vector example

 1: [[profiles::enforce(std::init)]];
 2: #include <memory>
 3: #include <cstdlib>
 4: // scaffolding: std::now_init exists in the GCC branch's <utility>; the Clang branch recognizes
 5: // std::now_init by name but ships no declaration, so one is supplied for it
 6: #include <utility>
 7: #ifdef __clang__
 8: namespace std { template<class T> T* now_init(T* p [[ref_to_uninit]]); }
 9: #endif
10: // scaffolding the sketch assumes
11: struct Bad_index {};
12: void* my_malloc [[ref_to_uninit]] (std::size_t);
13: using std::uninitialized_fill; using std::uninitialized_move; using std::now_init;
14: using std::malloc; using std::free; using std::construct_at;
15: 
16: template<class T>
17: struct Vector_memory {
18:     T* elem [[ref_to_uninit]];
19:     int no_of_elem = 0;
20:     int no_of_slots = 0;
21:     Vector_memory(int ne, int ns)
22:         : elem{ (T*)my_malloc((ne + ns) * sizeof(T)) },
23:           no_of_elem{ ne },
24:           no_of_slots{ ns }
25:     { }
26:     ~Vector_memory() { free(elem); }
27: };
28: 
29: template<class T>
30: class Vector {
31:     Vector_memory<T> mem;
32: public:
33:     Vector() {}
34:     Vector(int ne, int ns, const T& val = T{})
35:         : mem{ ne,ns }
36:     {
37:         // assume that uninitialized_fill() knows about [[ref_to_uninit]]:
38:         uninitialized_fill(mem.elem, mem.elem + ne, val);
39:     }
40:     T& operator[](int i)    // note the use of now_init()
41:     {
42:         if (0 <= i && i < mem.no_of_elem)
43:             return *now_init(mem.elem + i);   // this slot has been initialized
44:         throw Bad_index{};
45:     }
46:     ~Vector() { destroy(now_init(mem.elem), mem.no_of_elem); }
47:     void reserve(int newsz)
48:     {
49:         int n = mem.no_of_elem;
50:         if (newsz <= n + mem.no_of_slots) return;
51:         T* p [[ref_to_uninit]] = (T*)malloc(newsz * sizeof(T));
52:         T* old_elem = now_init(mem.elem);
53:         uninitialized_move(old_elem, old_elem + n, p);
54:         destroy(old_elem, n);
55:         free(mem.elem);
56:         mem.no_of_slots = newsz - n;
57:         mem.elem = (T*)p;
58:     }
59:     void push_back(const T& x)
60:     {
61:         if (mem.no_of_elem == mem.no_of_slots)
62:             reserve(mem.no_of_elem == 0 ? 8 : 2 * mem.no_of_elem);
63:         construct_at(mem.elem + mem.no_of_elem, x);
64:         ++mem.no_of_elem;
65:         --mem.no_of_slots;
66:     }
67:     void destroy(T*, int);
68: };
69: 
70: int use()
71: {
72:     Vector<int> v(2, 2, 7);
73:     v.push_back(1);
74:     return v[0];
75: }

2.1.1. What the paper says

P4222R3 §4.7, page 23:

Here is a much-simplified sketch of such a function template [the Vector_memory above].

Vector_memory knows only about uninitialized memory.

[The Vector class above.]

Here, I have assumed that malloc(), free(), etc. is known to the initialization profile. If not, equivalent functions with slightly different names that do use the initialization attributes must be used (§4.4).

The main point here is that the code is quite conventional (for this kind of C++ code). It is guaranteed free from initialization errors except for the call of now_init() call in the subscript operator. It is only called from within an initialized range but verifying that statically would be expensive and in some similar cases impossible. Having now_init() saves the user from suppressing the profile and offers an easy-to-spot point for code review.

The sketch is made compilable with the smallest edits: Int to int, a Bad_index type, a declaration of my_malloc, std:: qualification, and the member destroy declared as the paper has it. The function use is added. Expected: accepted.

2.1.2. What happens

The paper says the sketch "is guaranteed free from initialization errors except for the call of now_init()". Clang rejects line 57, mem.elem = (T*)p; in reserve: after uninitialized_move into p, p's referent is initialized, and a [[ref_to_uninit]] member may not be made to refer to initialized memory. elem refers to a mixed range, and the design has no marker for one. That is the design's central problem, and it is one line long. GCC reports five errors on four lines, all for the same reason: the standard library is not annotated. [Revised 2026-11-14: was "rejects five lines".] (T*)malloc(...) into a marked pointer is rejected (GCC treats malloc's result as initialized), free(elem) is rejected twice ("its parameter is not marked [[ref_to_uninit]]"), and std::uninitialized_fill(mem.elem, ...) is rejected on both iterator arguments. The paper's comment "assume that uninitialized_fill() knows about [[ref_to_uninit]]" is exactly the assumption that fails.

Clang:

57:18: error: pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory

GCC:

51:44: error: assigning a pointer not marked '[[ref_to_uninit]]' into a pointer marked '[[ref_to_uninit]]'
55:13: error: argument 1 to 'void free(void*)' refers to '[[uninit]]' memory but its parameter is not marked '[[ref_to_uninit]]'
26:28: error: argument 1 to 'void free(void*)' refers to '[[uninit]]' memory but its parameter is not marked '[[ref_to_uninit]]'
38:27: error: argument 1 to 'void std::uninitialized_fill(...)' refers to '[[uninit]]' memory but its parameter is not marked '[[ref_to_uninit]]'
38:27: error: argument 2 to 'void std::uninitialized_fill(...)' refers to '[[uninit]]' memory but its parameter is not marked '[[ref_to_uninit]]'

2.2. §1.4 — Range-for as initialization (aar6)

1: [[profiles::enforce(std::init)]];
2: int aar6_test()
3: {
4:     int aar6 [[uninit]] [10];
5:     for (int& x : aar6) x = 0b1010101010101010;
6:     return aar6[3];   // added: is the loop credited as initialization?
7: }

2.2.1. What the paper says

P4222R3 §1.4, page 8:

Range-for loops be are often simple to validate. For example [the loop above]. To allow that, the initialization profile would have to recognize the loop as an initialization, verify that the loop body really is initialization, and that the loop really complete.

The paper implies this should be accepted. A read after the loop is added to see whether the loop is credited as initialization.

2.2.2. What happens

Neither compiler recognizes the range-for as an initialization. Clang rejects the loop header on line 5 (the implicit __range reference binds uninitialized memory unmarked) and the read on line 6. GCC rejects the same header twice (once as a marker mismatch, once as address-taken-before-initialized) and the read. So the case the paper calls "simple to validate" is a hard error on both. The sanctioned way to fill the array is std::uninitialized_fill over the whole range, and that works on Clang only: GCC rejects std::uninitialized_fill(aar6, aar6 + 10, 0) and the std::ranges form as well, its library not being annotated. [Revised 2026-11-14: the first version gave std::uninitialized_fill as the way out without having tried it on GCC.]

Clang:

5:19: error: reference to uninitialized memory must be marked '[[ref_to_uninit]]'
6:12: error: read of a subobject of an '[[uninit]]' object accesses uninitialized memory

GCC:

5:19: error: assigning a pointer marked '[[ref_to_uninit]]' into a pointer not marked '[[ref_to_uninit]]'
5:19: error: address of '[[uninit]]' variable 'aar6' is taken here before it is provably initialized
6:18: error: 'aar6' read before it is definitely assigned

2.3. §1.4 — now_init to a span (aar10)

 1: [[profiles::enforce(std::init)]];
 2: #include <span>
 3: #include <memory>
 4: // scaffolding: std::now_init exists in the GCC branch's <utility>; the Clang branch recognizes
 5: // std::now_init by name but ships no declaration, so one is supplied for it
 6: #include <utility>
 7: #ifdef __clang__
 8: namespace std { template<class T> T* now_init(T* p [[ref_to_uninit]]); }
 9: #endif
10: struct X { int v; };
11: void any_initialization_technique(X*, int);
12: 
13: int aar10_test()
14: {
15:     X aar10 [[uninit]] [20];
16:     any_initialization_technique(aar10, 20);   // "... any initialization technique used here ..."
17:     std::span<X> s{std::now_init(aar10), 20};
18:     // ... use s and not aar10 here ...
19:     return s[0].v;   // added
20: }

2.3.1. What the paper says

P4222R3 §1.4, page 9:

Initialization too complex for the analyzer to recognize is best avoided but if really necessary can be handled by suppressing the initialization profile or using by (the unverified) now_init() (§4.6). For example [the function above]. Such suppression is an obvious target for code review and more advance analysis.

Expected: accepted; s is usable, and aar10 should not be used directly afterwards. now_init is spelled std::now_init, since §8.5 adds it to the standard library.

2.3.2. What happens

First, std::now_init does not exist in either standard library as shipped: the GCC branch adds it to <utility> (taking [[must_init]]), the Clang branch recognizes the name std::now_init but provides no declaration, so the scaffolding declares one for Clang. With that, both compilers reject line 16, the "any initialization technique" call, because passing aar10 to an unannotated X* parameter is itself the violation. The now_init line and the use of s are accepted by both. The example works only if the elided "…" is itself profile-clean, which for anything the paper would call "too complex for the analyzer" it will not be; now_init launders the result but not the route.

Clang:

16:34: error: pointer to uninitialized memory must be marked '[[ref_to_uninit]]'

GCC:

16:33: error: address of '[[uninit]]' variable 'aar10' is taken here before it is provably initialized

2.4. §5.3 — Label lab1 [[uninit]]; construct_at(&lab1, x, y, z)

 1: [[profiles::enforce(std::init)]];
 2: #include <string>
 3: #include <memory>
 4: using std::string;
 5: 
 6: struct Label {
 7:     string name;
 8:     string address;
 9:     int code;
10: };
11: 
12: void tst(const char* n, const char* a, int c, const char* x, const char* y, int z)
13: {
14:     Label lab0 { n, a, c };
15:     Label lab1 [[uninit]];
16:     // ...
17:     std::construct_at(&lab1, x, y, z);
18: }

2.4.1. What the paper says

P4222R3 §5.3, page 33:

For uninitialized class objects, we’ll have to use construct_at() rather than simple assignment to do the initialization. For example [the code above].

Expected: accepted.

2.4.2. What happens

Both reject Label lab1 [[uninit]];: the type has non-trivial default initialization (two std::string members), so it cannot be marked. GCC's second error, against the Label type ("constructor does not initialize member 'Label::code'"), is the same line again: it comes with a note that the synthesized default constructor was first required at lab1, and Label lab0 { n, a, c }; compiles on GCC by itself. The idiom the paper is illustrating, construct_at over an object whose string members are already alive, is a double construction: no destructor runs for the strings that were there. With both still empty, libstdc++ loses nothing; once one of them owns heap memory it leaks, which is what the runtime demo shows under AddressSanitizer. Both implementations refuse to let it be spelled. [Revised 2026-11-14: the first version said GCC rejects the Label type itself, so that lab0 could not be declared, and said the idiom "leaks when run" without saying that the demo first assigns a long string to lab1.name.]

Clang:

15:11: error: variable 'lab1' cannot be marked '[[uninit]]'; default-initialization of its type 'Label' does not leave it uninitialized

GCC:

15:11: error: 'lab1' cannot be marked '[[uninit]]': its type 'Label' has non-trivial default initialization
6:8: error: constructor does not initialize member 'Label::code', which is not marked '[[uninit]]'

2.5. §5.4 — Label: mixed members, the [[uninit]]-member repair, and the all-initialized form

 1: [[profiles::enforce(std::init)]];
 2: #include <string>
 3: using std::string;
 4: 
 5: struct Label {
 6:     string name;
 7:     string address;
 8:     int code;
 9: };
10: 
11: struct Label2 {             // "OK, but not for static objects"
12:     string name [[uninit]];
13:     string address [[uninit]];
14:     int code;
15: };
16: 
17: struct Label3 {             // "OK"
18:     string name;
19:     string address;
20:     int code = 0;
21: };
22: 
23: void f(const char* n, const char* a, int c)
24: {
25:     Label lab0 { n, a, c };     // OK
26:     Label lab1;                 // error: lab1.code uninitialized
27:     Label lab2 [[uninit]];      // error: lab1.name and lab1.address are default initialized
28:     Label2 lab2b [[uninit]];    // paper: would have been OK
29:     Label3 lab3;                // OK
30: }

2.5.1. What the paper says

P4222R3 §5.4, page 34:

Classes with a mix of initialized and uninitialized members give errors unless immediately initialized [the Label, lab0, lab1 and lab2 above].

The lab2 definition would have been OK if Label had been defined as [the Label2 above, which the paper comments “OK, but not for static objects”].

Usually, it would be better to ensure that either all members are default initialized or none [the Label3 above, which the paper comments “OK”].

The paper's three definitions of Label are renamed Label, Label2, and Label3 so they fit in one TU, and lab2b is added to declare the [[uninit]] object the paper says the repaired definition permits. Expected: lab0 OK; lab1 error (lab1.code uninitialized); lab2 error (name and address are default initialized); lab2b OK; lab3 OK.

2.5.2. What happens

lab1 and lab2 are rejected by both, as the paper says (GCC reports lab1's problem against the type Label rather than the variable). The repair the paper offers, Label2 with string name [[uninit]], is rejected by both at the member declarations: [[uninit]] cannot be put on a std::string because default-initialization does not leave it uninitialized. So Label2 lab2b [[uninit]], which the paper says "would have been OK", is an error on both, and there is no spelling in either implementation for leaving a member that has a default constructor uninitialized. A class that mixes an initialized member with an [[uninit]] scalar or array member is accepted by both (§4.5 Slot, §5.3 X_annotated). [Revised 2026-11-14: the first version said there was no spelling for a class with some members initialized and some not, which those two examples contradict.] Label3 (int code = 0) is accepted by both.

Clang:

12:19: error: member 'name' cannot be marked '[[uninit]]'; default-initialization of its type 'string' does not leave it uninitialized
13:22: error: member 'address' cannot be marked '[[uninit]]'; default-initialization of its type 'string' does not leave it uninitialized
26:11: error: variable 'lab1' must be initialized or marked '[[uninit]]'
27:11: error: variable 'lab2' cannot be marked '[[uninit]]'; default-initialization of its type 'Label' does not leave it uninitialized
28:12: error: variable 'lab2b' cannot be marked '[[uninit]]'; default-initialization of its type 'Label2' does not leave it uninitialized

GCC:

12:12: error: member 'Label2::name' cannot be marked '[[uninit]]': its type 'std::string' has non-trivial default initialization
13:12: error: member 'Label2::address' cannot be marked '[[uninit]]': its type 'std::string' has non-trivial default initialization
5:8: error: constructor does not initialize member 'Label::code', which is not marked '[[uninit]]'
27:11: error: 'lab2' cannot be marked '[[uninit]]': its type 'Label' has non-trivial default initialization
28:12: error: 'lab2b' cannot be marked '[[uninit]]': its type 'Label2' has non-trivial default initialization
11:8: error: constructor does not initialize member 'Label2::code', which is not marked '[[uninit]]'

2.6. §Appendix — Attribute placement: variables, pointers, arrays

 1: [[profiles::enforce(std::init)]];
 2: void f()
 3: {
 4:     int x [[uninit]];
 5:     int [[uninit]] y;       // not acceptable
 6:     [[uninit]] int z;
 7: 
 8:     int u [[uninit]];
 9:     int* p3 [[ref_to_uninit]] = &u;
10:     int* [[ref_to_uninit]] p4 = &u;
11:     int* p5 = &u [[ref_to_uninit]];
12: 
13:     int* [[ref_to_uninit]] arr1 [10];
14:     int* arr2 [[ref_to_uninit]] [10];
15:     int* arr3 [10] [[ref_to_uninit]];
16: }

2.6.1. What the paper says

P4222R3 Appendix: Attribute placement, page 40:

Consider these alternatives for placing [[uninit]] [the x, y and z above].

The middle alternative is unacceptable because under the rules for attribute placement it applies to the type, and we can’t modify the type. That would change the meaning of the program rather than just constrain it. The first and third alternatives follows the rules of attribute placement and have the right meaning. Unfortunately, this implies that we’ll get a variant of the const east west debate over aesthetics.

There are the same three alternatives for placing [[ref_to_uninit]]:

  • int* p3 [[ref_to_uninit]] = &x; Plausible: p3 is a pointer to something uninitialized.
  • int* [[ref_to_uninit]] p4 = &x; Plausible: p4 is an int* that points to something uninitialized,
  • int* p5 = &x [[ref_to_uninit]]; Odd: what is referencing something uninitialized, x or p5?

The same choice exists for arrays:

  • int* [[ref_to_uninit]] arr1 [10]; Plausible: the attribute is next to the type it describes.
  • int* arr2 [[ref_to_uninit]] [10]; Plausible: the attribute is just after the name as in variable definitions.
  • int* arr3 [10] [[ref_to_uninit]]; Plausible: the attribute is at the end, where the initializer would have been

Interestingly, in my examples, I had consistently used the first alternative for functions and the last for arrays. That’s not consistent, likely to generalize, or easy to explain with a single rule. The middle choice is that.

So, I place the attribute after the name (where the initializer is for initialized variables). Also, the attribute applies to the object rather than the type of the object.

Expected: x and z accepted, y rejected; p3 and p4 accepted, p5 doubtful; all three array forms accepted, with arr2 the one the paper settles on.

2.6.2. What happens

int [[uninit]] y is rejected by both (Clang error, GCC warning followed by an unmarked-variable error), as the paper says; [[uninit]] int z accepted by both. For pointers: p3 (after the name) is accepted by both; p4 (int* [[ref_to_uninit]] p4, "plausible") is rejected by Clang as an attribute on a type, and on GCC the attribute is dropped with a warning and the now-unmarked binding is then rejected as address-taken; p5 is a syntax error on both. For arrays, all three "plausible" placements are rejected by both compilers, including arr2, the after-the-name form the paper settles on: Clang says [[ref_to_uninit]] "only applies to pointers, references, non-union classes, and functions returning them", GCC the equivalent. An array of marked pointers cannot be declared on either implementation, although §4.2 lists "arrays of pointers" among the things that can be [[ref_to_uninit]].

Clang:

5:11: error: 'uninit' attribute cannot be applied to types
5:20: error: variable 'y' must be initialized or marked '[[uninit]]'
10:12: error: 'ref_to_uninit' attribute cannot be applied to types
11:18: error: an attribute list cannot appear here
13:12: error: 'ref_to_uninit' attribute cannot be applied to types
13:28: error: variable 'arr1' must be initialized or marked '[[uninit]]'
14:17: error: 'ref_to_uninit' attribute only applies to pointers, references, non-union classes, and functions returning them
14:10: error: variable 'arr2' must be initialized or marked '[[uninit]]'
15:22: error: 'ref_to_uninit' attribute cannot be applied to types
15:10: error: variable 'arr3' must be initialized or marked '[[uninit]]'

GCC:

5:9: warning: attribute ignored
5:20: error: local variable 'y' not initialized and not marked '[[uninit]]'
10:28: warning: 'ref_to_uninit' attribute does not apply to types
10:28: error: address of '[[uninit]]' variable 'u' is taken here before it is provably initialized
11:18: error: two consecutive '[' shall only introduce an attribute before '[' token
13:36: warning: 'ref_to_uninit' attribute does not apply to types
13:28: error: local variable 'arr1' not initialized and not marked '[[uninit]]'
14:36: error: 'ref_to_uninit' only supported on a pointer- or reference-typed parameter, variable, or non-static data member, or a function returning one
14:10: error: local variable 'arr2' not initialized and not marked '[[uninit]]'
15:36: warning: 'ref_to_uninit' attribute does not apply to types
15:10: error: local variable 'arr3' not initialized and not marked '[[uninit]]'

2.7. §Appendix — Attribute placement: the forms the paper settles on, and the rejected in-type forms

 1: [[profiles::enforce(std::init)]];
 2: #include <memory>
 3: using std::unique_ptr;
 4: 
 5: // what the paper settles on
 6: int* f1 [[ref_to_uninit]] (int);
 7: int arr [[uninit]] [7];
 8: void f2(int* [[ref_to_uninit]]);
 9: void f3(int* arg [[ref_to_uninit]]);
10: auto f4 [[ref_to_uninit]] (int) -> int*;
11: void g() { unique_ptr<int> up [[ref_to_uninit]]; }
12: void fct1 [[must_init]] (int* p [[ref_to_uninit]]);
13: 
14: // what the paper rejects
15: template<class T> struct smart_ptr { T* p; };
16: smart_ptr<int [[uninit]]> sp;          // error: [[uninit]] is not part of the type
17: void fct2(int [[uninit]]);             // error: argument passing is initialization
18: int fct3(int* [[ref_to_uninit]]);      // pointer to uninitialized int
19: void h() { int x [[uninit]]; int [[uninit]]* p6 = &x; }   // not proposed

2.7.1. What the paper says

P4222R3 Appendix: Attribute placement, page 42:

We cannot completely escape the placement problems because we have to deal with “smart pointers” and function declarations. For example [the paper's smart_ptr and fct lines, above as sp, fct2 and =fct3=].

Here, there is no name to which we can attach the attribute. However, the attribute will always come last, in the position after the name, had there been one. This is a point where implementer input is essential. I proceed on the assumption that this notation is manageable.

For now, I’m using what I consider the cleanest for arrays, functions, and template arguments; that is, annotations for return types are placed by the name and annotations on argument types after the argument type [the f1, arr, f2, f3, f4 and up above].

The placement of [[must_init]] follows the rule for return types: the attribute is next to the name it describes [the fct1 above].

The seven settled forms are spelled exactly as the paper has them. The paper's rejected forms follow: smart_ptr<int [[uninit]]> and void fct1(int [[uninit]]) are marked errors, int fct(int* [[ref_to_uninit]]) is given as the pointer-to-uninitialized spelling, and int [[uninit]]* p6 is from the "not proposed" list. The two rejected functions are renamed fct2 and fct3 so they do not collide with the settled fct1. Expected: the seven settled forms accepted; sp and fct2 rejected.

2.7.2. What happens

Of the seven forms the paper settles on: f1, f3 and f4 are accepted by both. int arr [[uninit]] [7] is rejected by both, not for placement but because the paper's example is a global and statics cannot be [[uninit]]. void f2(int* [[ref_to_uninit]]), the unnamed-parameter form, is an error on Clang (attribute on a type) and a warning on GCC (attribute dropped). unique_ptr<int> up [[ref_to_uninit]] is accepted by Clang and rejected by GCC ("only supported on a pointer- or reference-typed …"): class-type handles carry the marker on one implementation only. void fct1 [[must_init]] (...) is a warning on Clang (unknown attribute) and an error on GCC (wrong position). Of the four forms the paper rejects, Clang rejects all four as errors; GCC downgrades three to "attribute ignored" warnings (so void fct2(int [[uninit]]) compiles on GCC as plain void fct2(int)) and rejects only p6, as address-taken once its attribute is dropped. Of the Appendix's seven settled spellings, three work on both implementations, one (arr) is an error on both, and the other three (f2, up, fct1) are each an error on one compiler and accepted, or ignored with a warning, on the other. [Revised 2026-11-14: the first version counted two failing on both and three more in dispute; only arr is an error on both.]

Clang:

7:5: error: '[[uninit]]' cannot be applied to variable 'arr' with static storage duration; it is zero-initialized
8:16: error: 'ref_to_uninit' attribute cannot be applied to types
12:13: warning: unknown attribute 'must_init' ignored
16:17: error: 'uninit' attribute cannot be applied to types
17:17: error: 'uninit' attribute cannot be applied to types
18:17: error: 'ref_to_uninit' attribute cannot be applied to types
19:36: error: 'uninit' attribute cannot be applied to types

GCC:

7:22: error: 'uninit' on declaration other than automatic variable or non-static data member
8:30: warning: 'ref_to_uninit' attribute does not apply to types
11:47: error: 'ref_to_uninit' only supported on a pointer- or reference-typed parameter, variable, or non-static data member, or a function returning one
12:50: error: 'must_init' only supported on a pointer- or reference-typed parameter
16:24: warning: 'uninit' attribute does not apply to types
17:15: warning: attribute ignored
18:31: warning: 'ref_to_uninit' attribute does not apply to types
19:34: warning: attribute ignored
19:46: error: address of '[[uninit]]' variable 'x' is taken here before it is provably initialized

2.8. §4.9 — A user-written uninitialized_fill with a marked range parameter

 1: [[profiles::enforce(std::init)]];
 2: #include <ranges>
 3: #include <concepts>
 4: using std::ranges::forward_range; using std::ranges::range_value_t; using std::constructible_from;
 5: 
 6: template<forward_range R, class V>
 7:     requires constructible_from<range_value_t<R>, V>
 8: void uninitialized_fill(R& r [[ref_to_uninit]], V val);   // R& : the paper's by-value R decays the array
 9: 
10: int tst()
11: {
12:     int a1[] = {1,2,3};
13:     int a2 [[uninit]] [3];
14:     uninitialized_fill(a1, 10);     // error: a1 is already initialized
15:     int x = a2[0];                  // error
16:     uninitialized_fill(a2, 10);     // OK
17:     int y = a2[0];                  // OK. Now a2 is initialized
18:     return x + y;
19: }

2.8.1. What the paper says

P4222R3 §4.9, page 28:

By default, a template function’s argument must be initialized; if not, it must be marked [[uninit]]. A pointer argument must point to an initialized object; if not, it must be marked [[ref_to_uninit]]. For example [the template and the a1 and a2 calls above].

How do we know that R refers to something and what? After all, it isn’t a (built-in) pointer or reference. We don’t have to. Instead, when compiling uninitialized_fill(), writing through pointers (things that refer to elements) must be done using construct_at() or – for built-in types by simple assignment (see §5.2) or profile suppression must be used.

A page later the paper continues with the same array:

How does the compiler know that uninitialized_fill() initializes? [In the paper’s fragment int x = a2[0] is an error before the call and int y = a2[0] is “OK. Now a2 is initialized” after it.]

We could simply require that the compiler knows about the semantics of standard-library functions such as uninitilized_fill(), but that’s not general. Alternatively, we could use the attribute [[must_init]] (§6.2).

The two fragments are merged into one function. The paper's template is written template<forward_range R, auto V>; auto V would be a non-type parameter, so class V is used, range_value_t<T> is corrected to <R>, and R r by value, which would decay the array, becomes R&.

2.8.2. What happens

The paper's template needs two corrections to be C++ at all (auto V → class V, range_value_t<T> → <R>) and a third to work (R r by value decays the array; R& is used). With those, both compilers match the paper's first three annotations: uninitialized_fill(a1, 10) rejected, int x = a2[0] rejected, uninitialized_fill(a2, 10) accepted. Both then reject int y = a2[0] — the paper's "OK. Now a2 is initialized" — because a user-written [[ref_to_uninit]] function initializes nothing; the paper itself concedes this ("How does the compiler know … [[must_init]]"), so the example's last line contradicts its own text.

Clang:

14:24: error: reference marked '[[ref_to_uninit]]' must refer to uninitialized memory
15:13: error: read of a subobject of an '[[uninit]]' object accesses uninitialized memory
17:13: error: read of a subobject of an '[[uninit]]' object accesses uninitialized memory

GCC:

14:23: error: argument 1 to 'void uninitialized_fill(R&, V) [with R = int [3]; V = int]' must refer to '[[uninit]]' memory, matching its '[[ref_to_uninit]]' parameter
15:9: error: 'a2' read before it is definitely assigned
17:9: error: 'a2' read before it is definitely assigned

2.9. §5.5 — Arrays: arr1..arr4 and ranges::uninitialized_fill

 1: [[profiles::enforce(std::init)]];
 2: #include <memory>
 3: struct X { int v = 0; };    // has a default constructor
 4: X xval;
 5: 
 6: void f()
 7: {
 8:     int arr1[] = {1,2,3,4};
 9:     int arr2[10] [[uninit]];
10: 
11:     X arr3[20];                                 // error unless X has a default constructor
12:     std::ranges::uninitialized_fill(arr3, xval);     // error if X has a default constructor
13: 
14:     X arr4[20] [[uninit]];                      // error if X has a default constructor
15:     std::ranges::uninitialized_fill(arr4, xval);
16: }

2.9.1. What the paper says

P4222R3 §5.5, page 35:

Like structs, arrays can be initialized in the definition or marked [[uninit]]. For example [the arr1 and arr2 above].

However, it is quite common and quite reasonable for large arrays to have initialization delayed and initialized by – sometimes complex – algorithms (e.g., the input buffer example in §4.6).

The uninitialized_*() family of range algorithms are close to ideal for initializing arrays and can be verified, but using it is tricky. For example [the arr3 and its fill above].

So we get one error or the other.

Adding [[uninit]] helps when the programmer knows X [the arr4 and its fill above].

Note the placement int arr2[10] [[uninit]], after the bound. X is given a default constructor here, so the paper's comments predict: arr3 OK, the fill into arr3 an error, arr4 an error.

2.9.2. What happens

The paper's own placement int arr2[10] [[uninit]] (attribute after the bound) is rejected by both (Clang as an error, "'uninit' attribute cannot be applied to types"; GCC as a warning), after which arr2 is an unmarked uninitialized array and an error. The paper's Appendix settles on int arr [[uninit]] [7], so §5.5 contradicts the Appendix and both compilers side with the Appendix. For arr3 (X has a default constructor) Clang rejects the uninitialized_fill as the paper predicts; GCC accepts it. For arr4 both drop the misplaced attribute; Clang then rejects the fill again, GCC treats arr4 as an ordinary default-initialized array and accepts everything.

Clang:

9:20: error: 'uninit' attribute cannot be applied to types
9:9: error: variable 'arr2' must be initialized or marked '[[uninit]]'
12:37: error: reference marked '[[ref_to_uninit]]' must refer to uninitialized memory
14:18: error: 'uninit' attribute cannot be applied to types
15:37: error: reference marked '[[ref_to_uninit]]' must refer to uninitialized memory

GCC:

9:27: warning: 'uninit' attribute does not apply to types
9:9: error: local variable 'arr2' not initialized and not marked '[[uninit]]'
14:25: warning: 'uninit' attribute does not apply to types

3. Rejected by Clang where the paper says OK

Clang rejects a line the paper marks OK. GCC accepts that line, or differs on other lines of the same example.

3.1. §4.2 — Uninitialized pointers (p1, p2, p3)

1: [[profiles::enforce(std::init)]];
2: void f()
3: {
4:     int* p1;                // error: uninitialized
5:     int* p2 [[uninit]];     // p2 is uninitialized
6:     int* p3 = nullptr;
7: }

3.1.1. What the paper says

P4222R3 §4.2, page 15:

As ever, a reference cannot be uninitialized. The initialization profile requires the same for pointers unless marked [[uninit]]. When feasible, instead of leaving pointers uninitialized, use initialization to nullptr.

Expected: p1 error; p2 OK (p2 itself is uninitialized); p3 OK.

3.1.2. What happens

p1 is rejected by both. int* p2 [[uninit]], which the paper presents as the sanctioned way to leave a pointer uninitialized (and uses again as p4 in §4.3), is rejected by Clang: "'[[uninit]]' cannot be applied to a pointer; initialize the pointer (for example to 'nullptr')". GCC accepts it. The two implementations disagree on a line the paper gives twice.

Clang:

4:10: error: variable 'p1' must be initialized or marked '[[uninit]]'
5:15: error: '[[uninit]]' cannot be applied to a pointer; initialize the pointer (for example to 'nullptr')

GCC:

4:10: error: local variable 'p1' not initialized and not marked '[[uninit]]'

3.2. §4.2 — [[must_init]]: the callee must initialize

 1: [[profiles::enforce(std::init)]];
 2: struct X { int v; };
 3: 
 4: void f(X* p [[must_init]]);             // attribute on the parameter
 5: void f2 [[must_init]] (X* p [[ref_to_uninit]]);   // Appendix placement: on the function
 6: 
 7: X tst()
 8: {
 9:     X x [[uninit]];
10:     f(&x);
11:     // ... here *p is initialized ...
12:     X z = x;    // OK: x is initialized
13:     return z;
14: }
15: X tst2()
16: {
17:     X x [[uninit]];
18:     f2(&x);
19:     X z = x;    // OK per the Appendix spelling
20:     return z;
21: }
22: void does_not_initialize(X* p [[must_init]]) { }   // paper: error

3.2.1. What the paper says

P4222R3 §4.2, page 16:

We can require that all elements must be initialized: A [[must_init]] is a [[ref_to_uninit]] that must initialize all elements [the tst example above].

Naturally, it’s an error for a function that is passed a [[must_init]] not to initialize what’s referred to.

The paper's Appendix on attribute placement adds:

The placement of [[must_init]] follows the rule for return types: the attribute is next to the name it describes

  • void fct1 [[must_init]] (int* p [[ref_to_uninit]]);

Expected: after f(&x), X z = x; is OK. The §4.2 example puts the attribute on the parameter and the Appendix puts it after the function name, so both placements are included (tst and tst2), along with a callee that does not initialize its argument, which the paper says is an error.

3.2.2. What happens

The attribute has forked. Clang does not know [[must_init]] at all (three "unknown attribute ignored" warnings); it implements the same idea as [[now_init]]. With the attribute ignored, Clang then rejects f(&x) on line 10 as an unmarked pointer binding and the two X z = x reads, so the paper's "OK" lines are all errors on Clang. GCC implements [[must_init]], but only on the parameter: the Appendix's function-position spelling f2 [[must_init]] (...) is an error, the call on line 18 is then an ordinary [[ref_to_uninit]] call that initializes nothing, and the read on line 19 is rejected. [Revised 2026-11-14: the first version put the error at the call site; line 19 is the read.] GCC does check the callee, the one piece of §4.2 only GCC has: does_not_initialize is rejected with "function may return without the pointee of 'p' being definitely assigned".

Clang:

4:15: warning: unknown attribute 'must_init' ignored
5:11: warning: unknown attribute 'must_init' ignored
10:7: error: pointer to uninitialized memory must be marked '[[ref_to_uninit]]'
12:11: error: reference to uninitialized memory must be marked '[[ref_to_uninit]]'
19:11: error: reference to uninitialized memory must be marked '[[ref_to_uninit]]'
22:33: warning: unknown attribute 'must_init' ignored

GCC:

5:46: error: 'must_init' only supported on a pointer- or reference-typed parameter
19:7: error: 'x' read before it is definitely assigned
22:6: error: function may return without the pointee of 'p' being definitely assigned

3.3. §4.3 — f1/f2/g: pointer and reference flavors

 1: [[profiles::enforce(std::init)]];
 2: void f1(int* p [[ref_to_uninit]]);   // *p must be uninitialized
 3: void f2(int& r [[ref_to_uninit]]);   // r must refer to uninitialized
 4: 
 5: int* p0;    // static, thus initialized
 6: 
 7: void g(int x)
 8: {
 9:     int* p1;                            // error: uninitialized pointer
10:     int* p2 = nullptr;                  // OK
11:     int* p3 [[ref_to_uninit]] = &x;     // error: x is initialized
12:     f1(p2);                             // error: nullptr doesn't point to something uninitialized
13:     f1(p3);                             // OK
14:     int& r1 = x;                        // OK: x is initialized
15:     int y [[uninit]];
16:     int& r2 [[ref_to_uninit]] = y;
17:     f2(r1);                             // error: r1 refers to initialized
18:     f2(r2);                             // OK: r2 refers to uninitialized
19:     int* p4 [[uninit]];                 // p4 is uninitialized
20: }

3.3.1. What the paper says

P4222R3 §4.3, page 18:

The paper introduces the example with the single word "Consider" and lets the comments speak; they are reproduced in the source block. Expected: p0 static, thus initialized; p1 error; p2 OK; p3 error (x is initialized); f1(p2) error (nullptr doesn't point to something uninitialized); f1(p3) OK; r1 OK; r2 OK; f2(r1) error; f2(r2) OK; p4 is an uninitialized pointer and OK.

3.3.2. What happens

The paper annotates four errors (p1, p3, f1(p2), f2(r1)) and the rest OK. Clang: p1, p3 and f2(r1) rejected as the paper says; f1(p2) (passing nullptr to a [[ref_to_uninit]] parameter) is accepted (Clang's docs: a null handle "refers to nothing, so it suits either" marking); and int* p4 [[uninit]] is rejected (as in §4.2 pointers). GCC: p1, p3 and f2(r1) as the paper says, f1(p2) accepted like Clang; but f1(p3), which the paper marks OK, is rejected, and the binding int& r2 [[ref_to_uninit]] = y on line 16 is rejected as address-taken: GCC does not implement [[ref_to_uninit]] references to locals. GCC's rejection of f1(p3) follows from line 11: it goes by what p3 was bound to, the initialized x, where the paper and Clang go by the marker. With p3 bound to an [[uninit]] local, GCC accepts the call. [Revised 2026-11-14: this explanation was not in the first version, which left f1(p3) looking like a separate GCC restriction.] Neither compiler matches the paper's table, and they disagree with each other on three of the eleven lines (f1(p3), r2, p4).

Clang:

9:10: error: variable 'p1' must be initialized or marked '[[uninit]]'
11:10: error: pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory
17:8: error: reference marked '[[ref_to_uninit]]' must refer to uninitialized memory
19:15: error: '[[uninit]]' cannot be applied to a pointer; initialize the pointer (for example to 'nullptr')

GCC:

9:10: error: local variable 'p1' not initialized and not marked '[[uninit]]'
11:10: error: assigning a pointer not marked '[[ref_to_uninit]]' into a pointer marked '[[ref_to_uninit]]'
13:7: error: argument 1 to 'void f1(int*)' must refer to '[[uninit]]' memory, matching its '[[ref_to_uninit]]' parameter
16:10: error: address of '[[uninit]]' variable 'y' is taken here before it is provably initialized
17:7: error: argument 1 to 'void f2(int&)' must refer to '[[uninit]]' memory, matching its '[[ref_to_uninit]]' parameter

3.4. §5.2 — A member initialized in the constructor body ([[uninit]] int* p)

 1: [[profiles::enforce(std::init)]];
 2: constexpr int sys_max = 1000;
 3: void error(int);
 4: 
 5: class X {
 6:     int* p [[uninit]];
 7:     int x;
 8: public:
 9:     X(int v) : x{v}
10:     {
11:         if (v < 0 or sys_max <= v) error(v);
12:         p = new int(v);
13:     }
14:     int* get() { return p; }      // added: "p is still considered uninitialized" -- so is this an error?
15:     int  val() { return *p; }     // added
16: };
17: 
18: int use() { X x(3); return x.val() + *x.get(); }

3.4.1. What the paper says

P4222R3 §5.2, page 31:

If a member isn’t initialized in one of those two ways but rather initialized in the constructor body, it must be declared [[uninit]]. For example [the class X above].

Is p is still [[uninit]] after construction? To answer “no” (i.e., no longer [[uninit]]), the compiler would have to indulge in a form of non-local static analysis: Looking at all constructors to see if all paths through them initialized the [[uninit]] member. If any constructor wasn’t defined in the class’ TU it would not be possible. So, the answer is: p is still considered uninitialized.

Expected: the class accepted. A getter and a reader are added to see what "still considered uninitialized" means for a user of X.

3.4.2. What happens

Clang rejects the member declaration: [[uninit]] "cannot be applied to a pointer; initialize the pointer (for example to 'nullptr')". The paper's recommended pattern for a member initialized in the constructor body is unavailable for pointer members on Clang, and the paper's own next paragraph (the nullptr pseudo-initialization) is what Clang demands. GCC accepts the whole file: the constructor-body assignment, the getter returning p unmarked, and *p in val(). The paper's "p is still considered uninitialized" after construction is not something GCC enforces for a user of X.

Clang:

6:14: error: '[[uninit]]' cannot be applied to a pointer; initialize the pointer (for example to 'nullptr')

GCC:

(accepted, exit 0)

3.5. §4.9 — unique_ptr<T>(new T) and vector(10, x)

 1: [[profiles::enforce(std::init)]];
 2: #include <memory>
 3: #include <vector>
 4: #include <string>
 5: using std::unique_ptr; using std::vector;
 6: 
 7: int f()
 8: {
 9:     unique_ptr<std::string> ps (new std::string);   // new T, T has a default constructor
10:     unique_ptr<int> pi (new int);                   // new T, T = int: *pi is indeterminate
11:     int x [[uninit]];
12:     vector v(10, x);                                // error
13:     return *pi;                                     // added: read of the indeterminate int
14: }

3.5.1. What the paper says

P4222R3 §4.9, page 27:

Unfortunately, some class objects can be initialized with uninitialized “objects” and we must avoid breaking significant amounts of code that isn’t broken to provide guarantees. Consider:

unique_ptr<T> p (new T);

Is the T pointed to initialized or not (§4.1)? Do users of p care? They should care. The initialization profile must catch misuses. That implies that code like that must work as written if new T provides an initialized object and we must provide the user a way to handle the uninitialized cases. Unfortunately, make_unique() has equivalent problems relative to initialized/uninitialized.

Even vector is vulnerable [the two lines above, vector v(10,x) marked as an error].

The paper leaves new T as a question rather than a verdict; for T = int it does not provide an initialized object, so both T = std::string and T = int are included, with a read of *pi added. Expected: vector v(10, x) rejected.

3.5.2. What happens

Clang answers the paper's open question "Is the T pointed to initialized or not?" for T = int: unique_ptr<int> pi (new int) is rejected ("handle of type 'unique_ptr<int>' to uninitialized memory must be marked '[[ref_to_uninit]]'"), and vector v(10, x) is rejected as the paper says. GCC rejects only the vector line (as address-taken), and accepts new int into a unique_ptr and the read *pi: GCC does not treat new T of a scalar as uninitialized memory. The only line here that the paper annotates is the vector one, and both compilers reject it as marked. The new int lines answer a question the paper asks and leaves open, so Clang's rejection of pi is not against an "OK" in the paper, and this example sits in this group on weaker ground than its neighbours. [Revised 2026-11-14: the first version called Clang "stricter" and GCC "looser" on new int, as if the paper had ruled on it.]

Clang:

10:21: error: handle of type 'unique_ptr<int>' to uninitialized memory must be marked '[[ref_to_uninit]]'
12:18: error: reference to uninitialized memory must be marked '[[ref_to_uninit]]'

GCC:

12:19: error: address of '[[uninit]]' variable 'x' is taken here before it is provably initialized

4. Accepted by Clang where the paper says error

Clang accepts a line the paper marks as an error.

4.1. §4.6 — Lifetimes: construct_at into a span, indexed reads and double construction

 1: [[profiles::enforce(std::init)]];
 2: #include <span>
 3: #include <memory>
 4: struct X { int v; X(int v) : v(v) {} };
 5: 
 6: void init(std::span<X> s [[ref_to_uninit]], int i1, int i2)
 7: {
 8:     std::construct_at(&s[2], 10);   // only s[2] is initialized
 9:     X y = s[i2];                    // error: reading uninitialized? i2 could be != 2
10:     std::construct_at(&s[i1], 10);  // error: double initialization? i1 could be == 2
11: }

4.1.1. What the paper says

P4222R3 §4.6, page 22:

When the initialization profile is in force, the result of destruction is equivalent to an [[uninit]] annotation. It is an error to initialize an object twice (e.g., using construct_at()). Similarly, it is an error to destroy/uninitialize an object twice (e.g., using destroy_at()).

Initializing an object of a type with a constructor twice is an error, so construct_at() must require an uninitialized argument.

Enforcing this requires the simple flow analysis described in §1.2.

This relegates some unstructured techniques to code that suppresses the initialization profile. For example [the function above].

More realistic examples would involve loops and conditions.

The comments are the paper's: construct_at(&s[2],10) initializes only s[2]; X y = s[i2] is an error (reading uninitialized? i2 could be != 2); construct_at(&s[i1],10) is an error (double initialization? i1 could be = 2). The paper puts =[[ref_to_uninit]] on a span parameter.

4.1.2. What happens

Clang accepts construct_at(&s[2], 10) and rejects only the read X y = s[i2]. The second construct_at(&s[i1]), which the paper marks "error: double initialization?", is accepted (Clang's docs: an element credit through a handle "initializes nothing", so there is nothing for the second construction to collide with). GCC rejects the declaration: [[ref_to_uninit]] on a std::span parameter is "only supported on a pointer- or reference-typed parameter". The paper's premise, that a span can carry the marker (§4.2 lists spans, iterators, smart pointers), is implemented by Clang only.

Clang:

9:11: error: reference to uninitialized memory must be marked '[[ref_to_uninit]]'

GCC:

6:42: error: 'ref_to_uninit' only supported on a pointer- or reference-typed parameter, variable, or non-static data member, or a function returning one

4.2. §4.6 — Lifetimes: double destruction

 1: [[profiles::enforce(std::init)]];
 2: #include <span>
 3: #include <memory>
 4: struct X { int v; ~X(); };
 5: 
 6: void init(std::span<X> s, int i1, int i2)
 7: {
 8:     std::destroy_at(&s[2]);
 9:     std::destroy_at(&s[i1]);    // error: double destruction? i1 could be == 2
10: }

4.2.1. What the paper says

P4222R3 §4.6, page 22:

Destruction has a similar pattern and similar constraints. For example [the function above].

The comment is the paper's: the second destroy_at is an error (double destruction? i1 could be == 2). Expected: rejected.

4.2.2. What happens

Both accept the whole function. The paper's "error: double destruction? i1 could be = 2" is diagnosed by neither: destroys of /elements/ through a span or pointer are not judged (Clang's Known Gaps say so explicitly; GCC does not model =destroy_at at all).

Clang:

(accepted, exit 0)

GCC:

(accepted, exit 0)

5. GCC alone diverges

Clang gives the paper's verdict on every annotated line; only GCC differs, in one direction or the other. These are recorded, not held against a proof-of-concept branch.

5.1. §4.4 — The definition of now_init

1: [[profiles::enforce(std::init)]];
2: template<class T> T* now_init(T* p [[ref_to_uninit]]) { return p; }
3: 
4: int use()
5: {
6:     int u [[uninit]];
7:     int* p = now_init(&u);   // the user-defined one
8:     return *p;
9: }

5.1.1. What the paper says

P4222R3 §4.4, page 19:

We need a now_init() that takes a [[ref_to_uninit]] pointer and returns a pointer to initialized objects in the formerly uninitialized memory [the template above].

This function cannot be compiled with the initialization profile on because it provides a deliberate and explicit hole in the enforcement. It is a cast in disguise. Compiled without the initialization profile it is a no-op. If there was a way of writing it with the initialization profile on, then the profile would be flawed. Having now_init() provides the user a way to avoid suppressing all of the initialization profile and offers an easy-to-spot point for code review.

Expected: the definition is rejected when the profile is on. The function use is added to see what a caller gets if it is not.

5.1.2. What happens

Clang rejects the body of now_init ("cannot be compiled with the initialization profile on"), as the paper says. GCC accepts the whole file, including the read through the laundered pointer: its return p; from a [[ref_to_uninit]] parameter into an unmarked return type is not checked. The user's now_init works on GCC without suppression, which the paper says must be impossible ("If there was a way of writing it with the initialization profile on, then the profile would be flawed").

Clang:

2:64: error: pointer to uninitialized memory must be marked '[[ref_to_uninit]]'

GCC:

(accepted, exit 0)

5.2. §4.5 — class Wrong: one function, two kinds of return

1: [[profiles::enforce(std::init)]];
2: class Wrong {
3:     int x = 7;
4:     int y [[uninit]];
5: public:
6:     int& get(bool b) { return (b) ? x : y; }   // error
7: };

5.2.1. What the paper says

P4222R3 §4.5, page 20:

We have three choices for classes that hold objects for which pointers or references can be returned:

  • cannot return a reference to an uninitialized object (that must be the default rule).
  • always returns a reference to an uninitialized object (the analyzer must know that).
  • can return both initialized and uninitialized objects uniformly.

The third alternative cannot be implemented with a purely static guarantee [the class Wrong above].

Even if [[uninit]] had been part of the type, get() would be an error because a function can have only one return type. Separating the two alternatives requires some form of run-time test. Variants of this third alternative come up in several potential use cases.

Expected: get rejected.

5.2.2. What happens

Clang rejects the conditional return, as the paper says. GCC accepts it: a reference-returning member that mixes an initialized and an uninitialized member is not diagnosed, so the "third alternative" the paper says "cannot be implemented with a purely static guarantee" compiles cleanly on one of the two implementations.

Clang:

6:31: error: reference to uninitialized memory must be marked '[[ref_to_uninit]]'

GCC:

(accepted, exit 0)

5.3. §4.5 — class Slot: marked getter

 1: [[profiles::enforce(std::init)]];
 2: class Slot {
 3:     int x = 0b101010101010;
 4:     int y [[uninit]];
 5: public:
 6:     Slot (int xx) : x{ xx } {}
 7:     Slot() {}
 8:     int& get_init() { return x; }
 9:     int& get_uninit [[ref_to_uninit]] () { return y; }
10: };
11: 
12: void tst()
13: {
14:     Slot ii = 7;
15:     Slot uu;                                    // leaves uu.y uninitialized
16:     int i1 = ii.get_init();                     // OK
17:     int i2 = ii.get_uninit();                   // error: tries to read from an [[uninit]]
18:     int u1 [[uninit]] = uu.get_init();          // error: tries to initialize an [[uninit]]
19:     int& u2 [[ref_to_uninit]] = uu.get_uninit();    // OK
20:     u2 = 5;                                     // added: initialize y through u2
21:     int u3 = uu.get_uninit();                   // added: y is initialized now -- but the getter is marked
22: }

5.3.1. What the paper says

P4222R3 §4.5, page 20:

When returning an object, we must identify the uninitialized ones. The default must always be that what is returned is a properly initialized object. The uninitialized version must be marked as such. For example [the class Slot and its uses above].

At least until we can gather a lot of experience, we must choose a simple solution. Templatize Slot and you have one solution. That – like all initialization profile – is compile-time enforced.

The weakness of this approach is that you can’t ask a slot if it is initialized. That has to be managed externally.

Expected: i1 OK; i2 error (tries to read from an [[uninit]]); u1 error (tries to initialize an [[uninit]]); u2 OK. Two lines are added: initializing y through u2, and then reading it through the marked getter.

5.3.2. What happens

Clang matches all four of the paper's annotations, with u1 rejected for the §4.1 reason (both [[uninit]] and an initializer) rather than the "tries to initialize an [[uninit]]" the paper gives. The two added lines show the §4.5 weakness the paper admits: after u2 = 5 initializes y, uu.get_uninit() on line 21 is still rejected: a marked getter is uninitialized forever. GCC rejects only u1: ii.get_uninit() on line 17 (the paper's "error: tries to read from an [[uninit]]") is accepted, as is line 21. GCC does not check a read through a marked return value; it does check where a marked returned pointer is stored, as §4.3 malloc/calloc shows. [Revised 2026-11-14: the first version said GCC does not implement [[ref_to_uninit]] on return types.]

Clang:

17:17: error: read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory
18:9: error: variable 'u1' cannot be both '[[uninit]]' and have an initializer
21:17: error: read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory

GCC:

18:9: error: 'u1' is marked '[[uninit]]' but also has an initializer

5.4. §4.11 — Mixing malloc and new in one return

1: [[profiles::enforce(std::init)]];
2: #include <cstdlib>
3: int* f(int x)
4: {
5:     return (0 < x) ? (int*)std::malloc(x * sizeof(int)) : new int[x];
6: }

5.4.1. What the paper says

P4222R3 §4.11, page 29:

Consider mixing uninitialized and initialized data. This is problematic. The problem comes in the relatively few cases (in the global picture of things) where a class or a function needs to pass along either initialized or uninitialized arguments. Static initialized/uninitialized analysis cannot be perfect. For example [the function above].

Providing an incomplete guarantee, especially one that is often valid, leads to overconfidence and becomes a source of subtle bugs. That’s exactly the kind of problem the initialization profile (and other profiles) is aimed to prevent. So, if we want to do such mixing, we’d have to suppress the initialization profile.

Expected: rejected.

5.4.2. What happens

Clang rejects the mixed return, as the paper wants. GCC accepts it: it treats malloc as returning initialized memory, so for GCC there is no mixing to detect.

Clang:

5:12: error: pointer to uninitialized memory must be marked '[[ref_to_uninit]]'

GCC:

(accepted, exit 0)

5.5. §5.3 — Classes exposing uninitialized memory: struct X { Y m1; Y* m2; int arr[10]; }

 1: [[profiles::enforce(std::init)]];
 2: #include <cstdlib>
 3: struct Y { Y(int); };   // no default constructor
 4: 
 5: struct X {
 6:     Y m1;           // maybe error: maybe Y doesn't have a default constructor
 7:     Y* m2;          // error, see constructor
 8:     int arr[10];    // error, see constructor
 9:     X(int x) : m2{ (Y*)std::malloc(x) } {}
10: };
11: 
12: struct X_annotated {
13:     Y m1;                       // OK if Y has a default constructor
14:     Y* m2 [[ref_to_uninit]];
15:     int a [[uninit]] [10];
16:     X_annotated(int x) : m1{x}, m2{ (Y*)std::malloc(x) } {}
17: };

5.5.1. What the paper says

P4222R3 §5.3, page 32:

Some classes need to expose uninitialized members to users, e.g., some memory pools. Consider [the struct X above].

The initializer profile deems m1 (if Y doesn’t have a default constructor), m2 (malloc() returns a pointer to uninitialized memory), and arr (has uninitialized members) errors because they try to expose users of X to uninitialized objects without any indication of that.

Thus, we must add attributes [the X_annotated above].

Y is given no default constructor here, so the paper's "maybe error" on m1 is an error. The annotated form is renamed X_annotated so that both fit in one TU. Expected: the three members of X rejected; X_annotated accepted.

5.5.2. What happens

For the unannotated X, Clang reports three errors: m2 initialized from malloc (unmarked pointer to uninitialized memory), m1 not initialized (which is ordinary C++, since Y has no default constructor), and arr not initialized. GCC reports m1 as the ordinary C++ error and arr as the profile error, and says nothing about m2: malloc again. The annotated X_annotated is accepted by both.

Clang:

9:20: error: pointer to uninitialized memory must be marked '[[ref_to_uninit]]'
9:5: error: constructor for 'X' must explicitly initialize the member 'm1' which does not have a default constructor
9:5: error: constructor does not initialize member 'arr'

GCC:

9:39: error: no matching function for call to 'Y::Y()'
9:5: error: constructor does not initialize member 'X::arr', which is not marked '[[uninit]]'

5.6. §5.6 — Unions

 1: [[profiles::enforce(std::init)]];
 2: union S {
 3:     int x;
 4:     const char* s;
 5: };
 6: 
 7: void f()
 8: {
 9:     S x1 = {7};
10:     S x2 = {.s = "foo"};
11:     S x3;               // error: uninitialized
12:     S x4 [[uninit]];
13:     x4.s = "foo";       // OK? (no)
14: }

5.6.1. What the paper says

P4222R3 §5.6, page 35:

Consider the simplest solution (aligned with the handling of classes with public members) [the union and its uses above].

Here, the handling of x1 and x2 is simple and obvious. Should delayed initialization of x4 be accepted? No, to avoid treating union members of different types dramatically different.

Expected: x1 and x2 OK; x3 error (uninitialized); x4 [[uninit]] followed by x4.s = "foo", which the paper comments "OK? (no)". The paper's S x1 = 7; is not valid C++ for a union; braced forms are used.

5.6.2. What happens

Clang: x3 rejected ("variable 'x3' of union type must be initialized"), x4 [[uninit]] rejected ("cannot be applied to a variable of union type"), and the member write rejected: three errors, matching the paper's verdicts though not its reasons. GCC reports a single error, at x4: "cannot verify '[[uninit]]' on 'x4': member-level access on this aggregate is not yet analyzed", and does not diagnose S x3; at all.

Clang:

11:7: error: variable 'x3' of union type must be initialized
12:12: error: '[[uninit]]' cannot be applied to a variable of union type
13:10: error: writing a member of an '[[uninit]]' object does not initialize it; initialize the whole object

GCC:

12:7: error: cannot verify '[[uninit]]' on 'x4': member-level access on this aggregate is not yet analyzed

6. As the paper says

Both compilers give the paper's verdict on every annotated line.

6.1. §1.4 — Definite assignment: the maybe-maybe-maybe example (X = int)

 1: [[profiles::enforce(std::init)]];
 2: // scaffolding: the paper leaves X abstract; instantiate the 'X is int' bullet
 3: using X = int;
 4: 
 5: void f(int v)
 6: {
 7:     X x;        // maybe uninitialized
 8:     X y = x;    // maybe a read of an uninitialized
 9:     // ...
10:     x = v;      // maybe an initialization
11: }

6.1.1. What the paper says

P4222R3 §1.4, page 8:

Consider a simple example of the complexity we would face if initialization could be implicitly deferred and disguised as assignment [the example above]. Depending on what X is, this has different meanings:

  • If X is int, x is uninitialized and the initialization of y is UB in older compilers and erroneous behavior in C++26.

Imagine writing generic code with such a general definite-assignment extension to C++. If the extension was implemented only for the initialization profile, we’d get code that would have different meanings compiled with and without the profile. We chose to make “uninitialized” explicit and allow delayed initialization only when it doesn’t change semantics.

The paper lists five cases for X; the one for int is quoted. The paper leaves X abstract, so X = int is instantiated here. Under the profile the §4.1 rule makes the declaration itself an error (an uninitialized object not marked [[uninit]]), and the read of x an error.

6.1.2. What happens

Both reject, and both reject the declaration itself, under the §4.1 rule, before getting to the read that §1.4 discusses. Clang additionally reports the read of x on line 8; GCC stops at the declaration. The "maybe" in the paper's comments does not survive: under the profile there is nothing conditional about it.

Clang:

7:7: error: variable 'x' must be initialized or marked '[[uninit]]'
8:11: error: variable 'x' is read before initialization

GCC:

7:7: error: local variable 'x' not initialized and not marked '[[uninit]]'

6.2. §1.4 — C-style even/odd loops (aar7)

 1: [[profiles::enforce(std::init)]];
 2: int aar7_test()
 3: {
 4:     int aar7 [[uninit]] [10];
 5:     // ... no use of aar7 here ...
 6:     for (int x = 0; x < 10; x += 2) aar7[x] = 2;   // initialize even
 7:     // ... restricted use of aar7 here ...
 8:     for (int x = 1; x < 10; x += 2) aar7[x] = 1;   // initialize odd
 9:     return aar7[5];   // added
10: }

6.2.1. What the paper says

P4222R3 §1.4, page 8:

C-style loops are trickier. For example, I don’t recommend accepting this [the loops above].

Expected: rejected. A read after the loops is added.

6.2.2. What happens

Both reject, as the paper wants. Clang's message is the informative one: each element store is "writing an element of an [[uninit]] object does not initialize it; initialize the whole object". GCC reports the element stores as reads of aar7 (the subscript expression reads the array lvalue). The verdict is right for an odd reason.

Clang:

6:45: error: writing an element of an '[[uninit]]' object does not initialize it; initialize the whole object
8:45: error: writing an element of an '[[uninit]]' object does not initialize it; initialize the whole object
9:12: error: read of a subobject of an '[[uninit]]' object accesses uninitialized memory

GCC:

6:45: error: 'aar7' read before it is definitely assigned
8:45: error: 'aar7' read before it is definitely assigned
9:18: error: 'aar7' read before it is definitely assigned

6.3. §1.4 — while (cin && count<10) (aar8)

 1: [[profiles::enforce(std::init)]];
 2: #include <iostream>
 3: using std::cin;
 4: struct X { int v; };
 5: std::istream& operator>>(std::istream& is, X& x) { return is >> x.v; }
 6: 
 7: X aar8_test()
 8: {
 9:     X aar8 [[uninit]] [10];
10:     // ... no use of aar8 here ...
11:     int count = 0;
12:     while (cin && count < 10)
13:         cin >> aar8[count++];
14:     // ... we can use aar8 here ...
15:     return aar8[0];   // added
16: }

6.3.1. What the paper says

P4222R3 §1.4, page 9:

Even the simplest and most reasonable generalizations leave us with serious problems of verification. Consider [the loop above]. This is an example of the problem of determining what an initialization is and whether it is complete. Also, to verify this, we’d have to track the use of count and how would we handle the case where cin read only 5 values? Real-world examples can be far more complex. The idea of delayed initialization leads us to complexities.

Expected: rejected; the paper's next paragraphs route such initialization through a recognized initialization method, suppression of the profile, or now_init. The paper writes aar8[count++] << cin, which is not valid C++; cin >> aar8[count++] is used.

6.3.2. What happens

Both reject, as the paper wants. Clang rejects at cin >> aar8[count++] (binding an element to operator>>'s unmarked X&) and at the read; GCC only at the read on line 15. It accepted the extraction into the element: GCC treats the X& out-parameter binding as fine and simply never credits it.

Clang:

13:16: error: reference to uninitialized memory must be marked '[[ref_to_uninit]]'
15:12: error: reference to uninitialized memory must be marked '[[ref_to_uninit]]'

GCC:

15:18: error: 'aar8' read before it is definitely assigned

6.4. §3 — Static objects: f.c / g.c mutual recursion, and the four remedies

 1: [[profiles::enforce(std::init)]];
 2: // the paper's f.c and g.c, in one TU
 3: int f();
 4: int g();
 5: int x = f();          // paper: run-time initialization, must be rejected
 6: int y = g();          // paper: run-time initialization, must be rejected
 7: int f() { return y; }
 8: int g() { return x; }
 9: 
10: // the remedies the paper lists
11: struct X { int v; };
12: X init();
13: constinit int c1 = 7;                         // "Use only constinit global variables"
14: X& var() { static X v = init(); return v; }   // "Wrap statics in functions"
15: struct Z { consteval Z() : v(0) {} int v; };  // "Define default constructors to be consteval"
16: Z z;

6.4.1. What the paper says

P4222R3 §3, page 12:

Either order of initialization leads to an uninitialized variable being read. This must be avoided. Naturally, good developers avoid such examples, but not all examples are this trivial and we want initialization before use to be guaranteed. So

  • Every non-local variable with static or thread storage duration shall have static initialization. No run-time initialization is allowed for such objects.

This rule seems Draconian, but it conforms to common practice and has language support.

There are simple ways to obey this rule:

  • Don’t use global variables.
  • Use only constinit global variables.
  • Wrap statics in functions that guarantee initialization before use.
  • Define default constructors to be consteval.

Variations of all four techniques have been used for decades. Even constinit is just a recent direct support of an old technique.

The paper's two-TU example (f.c and g.c) is folded into one TU. Expected: int x = f(); and int y = g(); rejected; the constinit, function-local-static, and consteval forms accepted.

6.4.2. What happens

Both enforce the §3 rule exactly: int x = f(); and int y = g(); are errors ("requires constant initialization" / "has non-constant initialization"). The three remedies that are code are accepted; the fourth, not using global variables, leaves nothing to compile. [Revised 2026-11-14: was "All four remedies are accepted".] Note this is the one rule that applies to code with no [[uninit]] anywhere in it, and it rejects every namespace-scope object with a non-constexpr constructor.

Clang:

5:5: error: non-local variable 'x' requires constant initialization
6:5: error: non-local variable 'y' requires constant initialization

GCC:

5:5: error: 'x' has non-constant initialization
6:5: error: 'y' has non-constant initialization

6.5. §4.1 — [[uninit]] rules example (glob, loc, v1/v2, s1/s2, loc5, loc6)

 1: [[profiles::enforce(std::init)]];
 2: #include <vector>
 3: #include <string>
 4: using std::vector; using std::string;
 5: 
 6: int glob;                   // OK: default initialized
 7: int glob2 [[uninit]];       // error: initialized and [[uninit]]
 8: 
 9: void f()
10: {
11:     int loc;                // error: no initialization
12:     int loc2 [[uninit]];    // OK
13:     int loc3 = 3;           // OK
14:     int loc4 [[uninit]] = 4;    // error: initialized and [[uninit]]
15: 
16:     vector<int> v1;         // OK: default initialized
17:     vector<int> v2 [[uninit]];  // error: default initialized and [[uninit]]
18: 
19:     string s1;              // OK: default initialized
20:     string s2 [[uninit]];   // error: default initialized and [[uninit]]
21: 
22:     int loc5 [[uninit]];
23:     int x = loc5;           // error: read before initialized
24: 
25:     int loc6 [[uninit]];
26:     loc6 = 7;               // initializing
27:     int y = loc6;           // OK
28: }

6.5.1. What the paper says

P4222R3 §4.1, page 14:

Rules:

  • An uninitialized object not marked [[uninit]] is an error.
  • An initialized object marked [[uninit]] is an error.
  • An object marked [[uninit]] is left uninitialized and must be initialized before use (e.g., using construct_at(); §1.3). After initialization, the object is no longer [[uninit]].
  • A pointer or reference to an [[uninit]] can be passed only to a [[ref_to_uninit]] (§4.2).
  • A pointer or reference to something initialized can’t be passed to a [[ref_to_uninit]] (§4.2).
  • The function template now_init() can be used to provide a pointer to something initialized from a pointer to something previously uninitialized (§4.7).

For example [the function above].

We could imagine using [[uninit]] to suppress default initialization, but that would make code more error prone rather than less so.

The paper annotates every line, and the comments in the source block are its annotations: glob OK; glob2 error (initialized and [[uninit]]); loc error (no initialization); loc2 OK; loc3 OK; loc4 error; v1 OK; v2 error; s1 OK; s2 error; int x = loc5 error (read before initialized); loc6 = 7 is initialization and int y = loc6 OK.

6.5.2. What happens

Every line matches the paper's annotation on both compilers, with one difference of kind: the paper says glob2 is an error because it is "initialized and [[uninit]]"; both compilers say the attribute is simply not allowed on a static (Clang: "it is zero-initialized"; GCC: "'uninit' on declaration other than automatic variable or non-static data member"). The loc6 = 7; int y = loc6; pair is accepted by both. Both implement definite assignment for scalars, which §1.4 rejects by name.

Clang:

7:5: error: '[[uninit]]' cannot be applied to variable 'glob2' with static storage duration; it is zero-initialized
11:9: error: variable 'loc' must be initialized or marked '[[uninit]]'
14:9: error: variable 'loc4' cannot be both '[[uninit]]' and have an initializer
17:17: error: variable 'v2' cannot be marked '[[uninit]]'; default-initialization of its type 'vector<int>' does not leave it uninitialized
20:12: error: variable 's2' cannot be marked '[[uninit]]'; default-initialization of its type 'string' does not leave it uninitialized
23:13: error: variable 'loc5' is read before initialization

GCC:

7:20: error: 'uninit' on declaration other than automatic variable or non-static data member
11:9: error: local variable 'loc' not initialized and not marked '[[uninit]]'
14:9: error: 'loc4' is marked '[[uninit]]' but also has an initializer
17:17: error: 'v2' cannot be marked '[[uninit]]': its type 'std::vector<int>' has non-trivial default initialization
20:12: error: 's2' cannot be marked '[[uninit]]': its type 'std::string' has non-trivial default initialization
23:9: error: 'loc5' read before it is definitely assigned

6.6. §4.2 — [[ref_to_uninit]] parameters: f/g/tst

 1: [[profiles::enforce(std::init)]];
 2: struct X { int v; };
 3: 
 4: void f(X* p [[ref_to_uninit]]);   // p must point to something uninitialized
 5: void g(X* p);                     // q must point to something initialized
 6: 
 7: void tst()
 8: {
 9:     X x [[uninit]];
10:     f(&x);      // OK
11:     g(&x);      // error
12:     X y = { 7 };
13:     f(&y);      // error
14:     g(&y);      // OK
15: }

6.6.1. What the paper says

P4222R3 §4.2, page 15:

We introduce [[ref_to_uninit]], “reference to uninitialized”, to help correctly manipulate areas of uninitialized memory. For more examples, see §4.7 and §4.8. Manipulating a mix of initialized and uninitialized memory is error-prone and almost always under performance constraints. It would be far simpler to demand that all elements of an array are initialized (or not), but that would leave major critical areas of C++ use unserved. We get [the example above]. That is

  • An [[uninit]] can only be referred to by a [[ref_to_uninit]]
  • An object that is initialized cannot be referred to by a [[ref_to_uninit]]
  • The result of dereferencing a [[ref_to_uninit]] is [[uninit]]

Expected: f(&x) OK; g(&x) error; f(&y) error; g(&y) OK.

6.6.2. What happens

Both match the paper's four annotations: g(&x) and f(&y) rejected, f(&x) and g(&y) accepted. GCC's wording for g(&x) is "address taken before provably initialized" rather than a marker mismatch, but the verdicts agree.

Clang:

11:7: error: pointer to uninitialized memory must be marked '[[ref_to_uninit]]'
13:7: error: pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory

GCC:

11:6: error: address of '[[uninit]]' variable 'x' is taken here before it is provably initialized
13:6: error: argument 1 to 'void f(X*)' must refer to '[[uninit]]' memory, matching its '[[ref_to_uninit]]' parameter

6.7. §4.2 — A [[ref_to_uninit]] call does not initialize

 1: [[profiles::enforce(std::init)]];
 2: struct X { int v; };
 3: void f(X* p [[ref_to_uninit]]);   // p must point to something uninitialized
 4: 
 5: X tst()
 6: {
 7:     X x [[uninit]];
 8:     f(&x);
 9:     // ... here *p is not considered initialized ...
10:     X z = x;    // error: x is still [[uninit]]
11:     return z;
12: }

6.7.1. What the paper says

P4222R3 §4.2, page 16:

A [[ref_to_uninit]] need not be initialized. It may be passed on to another [[ref_to_uninit]] or it may be used in a way that doesn’t initialize every element (e.g., memory held by a vector).

Consider [the example above].

The comments in the source block are the paper's: after the call, *p is not considered initialized, and X z = x; is an error because x is still [[uninit]]. The paper declares x at namespace scope, which contradicts §3 and §4.1; it is a local here.

6.7.2. What happens

Both reject X z = x; as the paper says. This is the class-typed form; the scalar form of the same shape (int u [[uninit]]; f(&u); return u;) is accepted by Clang, because its plain-local analysis treats the call as an escape that initializes u (see the companion probes). The paper's example happens to be the one Clang gets right.

Clang:

10:11: error: reference to uninitialized memory must be marked '[[ref_to_uninit]]'

GCC:

10:7: error: 'x' read before it is definitely assigned

6.8. §4.3 — void* defaults to initialized

1: [[profiles::enforce(std::init)]];
2: void f()
3: {
4:     int x1 = 7;
5:     void* p1 = &x1;     // OK
6:     int x2 [[uninit]];
7:     void* p2 = &x2;     // error
8: }

6.8.1. What the paper says

P4222R3 §4.3, page 17:

From about 1983, we have void* to point to memory of unknown type. Unfortunately, for compatibility reasons, by default, we must assume that void* points for something initialized. For example [the function above].

All void*=s require casting to access what they point to, but what is pointed to by =p1 and p2 is fundamentally different from the perspective of initialization. However, the type system doesn’t reflect that.

Expected: p1 OK; p2 error.

6.8.2. What happens

Both reject void* p2 = &x2; and accept p1, as the paper says.

Clang:

7:11: error: pointer to uninitialized memory must be marked '[[ref_to_uninit]]'

GCC:

7:11: error: address of '[[uninit]]' variable 'x2' is taken here before it is provably initialized

6.9. §4.3 — Annotating malloc and calloc

 1: [[profiles::enforce(std::init)]];
 2: #include <cstddef>
 3: // the paper's proposed declarations (not including <cstdlib>, which declares them unmarked)
 4: void* malloc [[ref_to_uninit]] ( std::size_t size );
 5: void* calloc( std::size_t num, std::size_t size );
 6: void* uninit_malloc [[ref_to_uninit]] ( std::size_t size );
 7: 
 8: int f()
 9: {
10:     int* a [[ref_to_uninit]] = (int*)malloc(sizeof(int));   // OK
11:     int* b = (int*)malloc(sizeof(int));                     // error: unmarked
12:     int* c = (int*)calloc(1, sizeof(int));                  // OK: zero-initialized
13:     int* d [[ref_to_uninit]] = (int*)uninit_malloc(4);      // OK
14:     *a = 1;
15:     return *a + *c;
16: }

6.9.1. What the paper says

P4222R3 §4.3, page 17:

Consider the most common C-style allocators:

  • malloc() returns a void* pointing to uninitialized memory
  • calloc() returns a void* pointing to zero-initialized memory

The obvious solution would be to define [the first two declarations above]. To require that, we would have to explore if the addition of [[ref_to_uninit]] to all allocators is manageable. The problem is that some system headers might not be modified for the benefit of C++ profiles. In that case, either functions like malloc() will have to be called only in code where the initialization profile is suppressed or such functions must be known to an analyzer enforcing the initialization profile. The latter will already be the case in some implementations because of erroneous behavior implementation.

If we can’t add [[ref_to_uninit]] to malloc(), we must find a new name to reflect the fact [the uninit_malloc above].

The function f is added to exercise the declarations: a marked pointer from malloc is fine, an unmarked one is not, and calloc's result is initialized. <cstdlib> is deliberately not included, since it declares malloc unmarked.

6.9.2. What happens

With the paper's proposed annotated declarations in scope (and <cstdlib> not included), both compilers behave as the paper describes: the unmarked b is rejected, a, c, d accepted. This is the one place the design works as drawn, because the declarations were written for it. With the real <cstdlib>, Clang knows malloc by builtin ID and treats its result as uninitialized; GCC does not, and treats malloc's result as initialized memory (see the companion probes: GCC accepts int* p = (int*)malloc(4); return *p;).

Clang:

11:10: error: pointer to uninitialized memory must be marked '[[ref_to_uninit]]'

GCC:

11:26: error: assigning a pointer marked '[[ref_to_uninit]]' into a pointer not marked '[[ref_to_uninit]]'

6.10. §4.8 — The input-buffer example

 1: [[profiles::enforce(std::init)]];
 2: #include <span>
 3: #include <memory>
 4: #include <cstddef>
 5: #include <cstdlib>
 6: // scaffolding: std::now_init exists in the GCC branch's <utility>; the Clang branch recognizes
 7: // std::now_init by name but ships no declaration, so one is supplied for it
 8: #include <utility>
 9: #ifdef __clang__
10: namespace std { template<class T> T* now_init(T* p [[ref_to_uninit]]); }
11: #endif
12: constexpr int imax = 4096;
13: void* my_malloc [[ref_to_uninit]] (std::size_t);
14: void process(const struct Message&);
15: 
16: class Message {
17:     // operations to read typed members from a byte buffer rep
18:     std::byte rep[16];
19: };
20: 
21: // read at most max bytes into *p; return the number of bytes read:
22: int read(std::byte* p [[ref_to_uninit]], int max);
23: void destroy(Message&);    // maybe scrub buffer area
24: 
25: void tst(int live)
26: {
27:     // memory from some buffer pool:
28:     std::byte* inbuf [[ref_to_uninit]] = (std::byte*) my_malloc(imax);
29:     while (live) {   // read into inbuf and process the messages stored as bytes
30:         int n = read(inbuf, imax);   // some input function
31:         std::span<Message> messages {
32:             (Message*)std::now_init(inbuf), n / sizeof(Message)
33:         };
34:         // this buffer now contains Messages:
35:         for (Message& m : messages) {
36:             process(m);   // ... process a message from the buffer ...
37:             destroy(m);
38:         }
39:         --live;
40:     }
41: }

6.10.1. What the paper says

P4222R3 §4.8, page 25:

Consider another important use case, recycling a buffer. Here, reading or writing of a message should be once only and allocation should not be done while passing a stream of messages. Such code is – in many variations – used in much high performance and low latency applications. Again, this is just a sketch illustrating the techniques as they relate to initialization [the code above].

Note that assigning to an uninitialized std::byte, as read must do, is allowed. In addition to initialization, we require at least one explicit type conversion (bytes to messages done in now_init()) and must ensure that we don’t have range errors. This is the kind of low-level code where various forms of suppression of guarantees are tempting (and common in “safe” languages).

Expected: accepted. Scaffolding: Message's rep member, imax, my_malloc, process, and destroy(Message&) as the paper declares it.

6.10.2. What happens

Both accept, as the paper says, once std::now_init is available (scaffolding, see §1.4 now_init to a span) and with my_malloc declared marked. This and §4.3 malloc/calloc are the two examples where the paper's design and both implementations agree on a non-trivial program; both depend on the author controlling every declaration involved.

Clang:

(accepted, exit 0)

GCC:

(accepted, exit 0)

6.11. §5.2 — Member initializers: the accepted form

1: [[profiles::enforce(std::init)]];
2: class X {
3:     int m1 = 7;
4:     int m2;
5: public:
6:     X(int x) : m2{x} {}
7: };

6.11.1. What the paper says

P4222R3 §5.2, page 31:

The simplest and most direct way of initializing a member is through explicit initialization. For example [the class X above].

This is what the initialization profile requires.

Expected: accepted.

6.11.2. What happens

Both accept.

Clang:

(accepted, exit 0)

GCC:

(accepted, exit 0)

6.12. §5.2 — The three alternatives: nullptr pseudo-initialization, unique_ptr, default-constructed strings

 1: [[profiles::enforce(std::init)]];
 2: #include <memory>
 3: #include <string>
 4: constexpr int sys_max = 1000;
 5: void error(int);
 6: 
 7: class X1 {
 8:     int* p = nullptr;
 9:     int x;
10: public:
11:     X1(int v) : x{v}
12:     {
13:         if (v < 0 or sys_max <= v) error(v);
14:         p = new int(v);
15:     }
16: };
17: 
18: class X2 {
19:     std::unique_ptr<int> p;
20: public:
21:     X2(int x) : p{std::make_unique<int>(x)} {}
22: };
23: 
24: class X3 {
25:     std::string s1;
26:     std::string s2;
27: public:
28:     X3(const char* ss1, const char* ss2) : s1{ss1} { s2 = ss2; }
29: };

6.12.1. What the paper says

P4222R3 §5.2, page 31:

Often, using a “pseudo initialization” is a better choice. For example [the X1 above].

In most such cases, the optimizer can eliminate the redundant initialization.

Alternatively, we could use now_init() or suppression to expose that unverified p to users.

Often, an even better alternative is to use a member of a type that performs its own checks rather than more primitive types. For example [the X2 above].

Default construction works as ever. For example [the X3 above].

The use of the member initialization (for s1) is preferred to the assignment (of s2) as being more readable and more efficient unless the optimizer is smart enough to eliminate the default initialization of s2.

All three are presented as acceptable. Expected: accepted. The paper's make_unique(x) is missing its template argument; make_unique<int>(x) is used.

6.12.2. What happens

Both accept all three.

Clang:

(accepted, exit 0)

GCC:

(accepted, exit 0)

6.13. §5.4 — Classes without constructors: S s1..s4

 1: [[profiles::enforce(std::init)]];
 2: #include <string>
 3: using std::string;
 4: struct S { int x; string s; };
 5: 
 6: void f()
 7: {
 8:     S s1 = {1, "foo"};  // OK
 9:     S s2 = {1};         // OK, s2.s defaults to ""
10:     S s3;               // error: s3.x uninitialized
11:     s3.x = 1;
12:     s3.s = "foo";
13:     S s4 [[uninit]];    // error: s4.s is default constructed
14:     s4.x = 1;
15:     s4.s = "foo";
16: }

6.13.1. What the paper says

P4222R3 §5.4, page 33:

When defining objects of classes without constructors we must initialize all members in their definitions. For example [the function above].

The member s4.s is a string, and string has a default constructor. That means that S s4 [[uninit]]; declares s4.s as both initialized and uninitialized. That’s an error (even before we start considering delayed initialization).

Immediate initialization is simpler and clearer.

Don’t introduce a variable until you have an initializer for it.

Expected: s1 OK; s2 OK (s2.s defaults to ""); s3 error (s3.x uninitialized), followed by member assignments; s4 error (s4.s is default constructed), followed by member assignments.

6.13.2. What happens

Clang: s3 rejected as the paper says; s4 [[uninit]] rejected because S default-initializes (the paper's reason, differently phrased); and then both member assignments to s4 are rejected too. GCC: s4 rejected for the same reason. S s3; is reported not at line 10 but against the type at line 4 ("constructor does not initialize member 'S::x'"), with a note that the synthesized default constructor was first required at line 10. That is the paper's error on s3, reported where the constructor is and not where it is used; s1 and s2 compile on GCC by themselves. GCC says nothing about the member assignments to s4. [Revised 2026-11-14: the first version read the line-4 error as GCC rejecting the type S however it is used, s1 and s2 included, and filed this example with the ones where GCC alone diverges. Compiling s1 and s2 without s3 shows otherwise, and the example has moved here.]

Clang:

10:7: error: variable 's3' must be initialized or marked '[[uninit]]'
13:7: error: variable 's4' cannot be marked '[[uninit]]'; default-initialization of its type 'S' does not leave it uninitialized
14:10: error: writing a member of an '[[uninit]]' object does not initialize it; initialize the whole object
15:8: error: calling member function 'operator=' binds its implicit object parameter to uninitialized memory

GCC:

4:8: error: constructor does not initialize member 'S::x', which is not marked '[[uninit]]'
13:7: error: 's4' cannot be marked '[[uninit]]': its type 'S' has non-trivial default initialization

6.14. §5.4 — struct Point: member-wise initialization is banned

 1: [[profiles::enforce(std::init)]];
 2: struct Point { int x, y; };
 3: 
 4: int f()
 5: {
 6:     Point p [[uninit]];
 7:     // ...
 8:     p.x = 7;    // error: member assignment
 9:     // ...
10:     p.y = 9;    // error: member assignment
11:     return p.x + p.y;   // added
12: }

6.14.1. What the paper says

P4222R3 §5.4, page 34:

It would be possible to initialize and validate a struct with no constructor and no members with constructors. For example [the code above].

However, that adds complexity and opens opportunities for errors and confusion so unless we find genuine need, this is banned.

Expected: p.x = 7 error; p.y = 9 error. A read of both members is added.

6.14.2. What happens

Both reject the two member assignments, as the paper says. Clang also rejects the added read. The companion probes show the same ban reached through pointers: int* px [[ref_to_uninit]] = &p.x; *px = 7; is accepted at the store but credits nothing.

Clang:

8:9: error: writing a member of an '[[uninit]]' object does not initialize it; initialize the whole object
10:9: error: writing a member of an '[[uninit]]' object does not initialize it; initialize the whole object
11:14: error: read of a subobject of an '[[uninit]]' object accesses uninitialized memory
11:20: error: read of a subobject of an '[[uninit]]' object accesses uninitialized memory

GCC:

8:9: error: member 'Point::x' of 'p' is written before 'p' is initialized as a whole
10:9: error: member 'Point::y' of 'p' is written before 'p' is initialized as a whole

7. Not reproduced

Three of the paper's code fragments are not translation units under any reasonable completion and are omitted:

  • §4.6's vector<Slot> v; vector<Live> live_slot; ... if (live_slot[x]==Live.init) { int xx = v[x]; ... }: Live.init is not C++, and v[x] is a Slot, not an int; the paper offers it as a sketch of a run-time technique the profile cannot verify.
  • §4.10's concept-based overload set (template<Pointer_to_uninit T> int f(T* p)): the paper itself calls it an "unattainable ideal", since "the information for doing that is not available to a concept".
  • §1's opening figure of "errors caught by the GCC and Clang implementations" is an image in the PDF, not text; its content is covered by §4.1 rules.

8. Companion probes

The probes that go beyond the paper's own examples — two pointers to one object, aliases of the pointer object, lambda captures, two references to one object, this aliases in constructors, pointers to members of an [[uninit]] aggregate, [[uninit]] members reached through a pointer to the whole object, a partial memset, void* storage from malloc, double construction, and correlated conditions — are in a post of their own, with a Compiler Explorer link for each group; the §5.3 runtime leak demonstration is at https://godbolt.org/z/szzP1zWxe. [Revised 2026-11-14: the first version pointed at a file and an earlier set of links that were not published. It also listed by-value copies, the -Wmaybe-uninitialized shapes, and GCC's self-documented nested-diamond false positive; those are not among the published probes.]

9. Method

Each source was compiled through Compiler Explorer's API on 2026-10-25 against the two compilers above and the diagnostics recorded; the links were then created with CE's shortener using a full layout state (editor; per compiler, a compiler pane over an output pane) and each link's stored source checked against the compiled source. Diagnostics shown are trimmed of ANSI colour, source-echo and caret lines, context lines and notes, Clang's trailing warning-flag name, the "under profile 'std::init'" (Clang) or "under the 'std::init' profile" (GCC) that every message carries, and the "aka" expansion after a type name. The std::uninitialized_fill signature in the §4.7 listing is cut to (...), and four listings are in line order where the compiler emitted them otherwise. Nothing else is edited. [Revised 2026-11-14: the first version listed fewer trims than were made.]

On 2026-11-14 all 36 sources were compiled again and each listing compared with the fresh output, and each link's stored source and compiler settings compared with the post. Everything matched. The claims in the prose that a listing does not show by itself were then each tried with a small program of their own; where one failed, the passage is revised and says so.

10. Summary table

All 36 examples in the paper's order; each section number links to its example above.

A legend for the one-line verdicts:

as paper
the compiler's accept/reject verdicts match the paper's annotations.
stricter
rejects something the paper marks OK.
looser
accepts something the paper marks as an error.
diverge
the two implementations disagree with each other.
revised
the cell changed in the 2026-11-14 recheck; the example's own section says how.
§ Example Clang GCC Agree?
§1.4 Definite assignment: the maybe-maybe-maybe example (X = int) as paper as paper yes
§1.4 Range-for as initialization (aar6) stricter stricter yes
§1.4 C-style even/odd loops (aar7) as paper as paper yes
§1.4 while (cin && count<10) (aar8) as paper as paper yes
§1.4 now_init to a span (aar10) stricter (the elided step) stricter (the elided step) yes
§3 Static objects: f.c / g.c mutual recursion, and the four remedies as paper as paper yes
§4.1 [[uninit]] rules example (glob, loc, v1/v2, s1/s2, loc5, loc6) as paper as paper yes
§4.2 Uninitialized pointers (p1, p2, p3) stricter: no [[uninit]] on pointers as paper no
§4.2 [[ref_to_uninit]] parameters: f/g/tst as paper as paper yes
§4.2 A [[ref_to_uninit]] call does not initialize as paper as paper yes
§4.2 [[must_init]]: the callee must initialize [[must_init]] unknown [[must_init]] on parameter only no
§4.3 void* defaults to initialized as paper as paper yes
§4.3 Annotating malloc and calloc as paper as paper yes
§4.3 f1/f2/g: pointer and reference flavors looser (nullptr), stricter (p4) looser (nullptr), stricter (p3 use, r2) no
§4.4 The definition of now_init as paper looser: now_init compiles no
§4.5 class Wrong: one function, two kinds of return as paper looser no
§4.5 class Slot: marked getter as paper looser: marked getters unchecked no
§4.6 Lifetimes: construct_at into a span, indexed reads and double construction looser (double construct_at) span cannot carry marker no
§4.6 Lifetimes: double destruction looser looser yes
§4.7 The Vector example stricter: rejects the Vector stricter: rejects the Vector (×5) no (different lines)
§4.8 The input-buffer example as paper as paper yes
§4.9 unique_ptr<T>(new T) and vector(10, x) rejects new int, which the paper leaves open (revised) accepts new int and the read (revised) no
§4.9 A user-written uninitialized_fill with a marked range parameter stricter on last line (paper self-contradicts) same yes
§4.11 Mixing malloc and new in one return as paper looser no
§5.2 Member initializers: the accepted form as paper as paper yes
§5.2 A member initialized in the constructor body ([[uninit]] int* p) stricter: no [[uninit]] on pointer member looser: nothing checked no
§5.2 The three alternatives: nullptr pseudo-initialization, unique_ptr, default-constructed strings as paper as paper yes
§5.3 Classes exposing uninitialized memory: struct X { Y m1; Y* m2; int arr[10]; } as paper looser (malloc) no
§5.3 Label lab1 [[uninit]]; construct_at(&lab1, x, y, z) stricter: example rejected stricter: example rejected (revised) yes (revised)
§5.4 Classes without constructors: S s1..s4 as paper as paper: s3 reported at the type (revised) yes (revised)
§5.4 struct Point: member-wise initialization is banned as paper as paper yes
§5.4 Label: mixed members, the [[uninit]]-member repair, and the all-initialized form stricter: repair rejected stricter: repair rejected yes
§5.5 Arrays: arr1..arr4 and ranges::uninitialized_fill §5.5 placement rejected §5.5 placement rejected; fill accepted no
§5.6 Unions as paper looser (x3) / 'not yet analyzed' no
§Appendix Attribute placement: variables, pointers, arrays array forms rejected array forms rejected mostly
§Appendix Attribute placement: the forms the paper settles on, and the rejected in-type forms 2 of 7 settled forms fail, 1 more ignored (revised) 3 of 7 fail, 1 more ignored (revised) no

✌Existing Practice✌

"Standardize existing practice" was barely true of the first C++ standard, and it is the wrong rule now. It was already being stretched in 1998. The ARM was existing practice. Templates as specified in the ARM were not what got standardized; the STL had two years of use at HP and a handful of early adopters before it was voted in; namespaces, exceptions as shipped, and iostreams with locales were designed in committee and implemented afterward. The result was a standard that no compiler implemented for years. We call it a success because C++ survived it, not because the process was sound.

Read more…

Scrap Your static_assert

The obvious way to test a compile-time fact is static_assert. It's right there, it needs no framework, and for a fact that has to hold it's the right tool. As a test, though, it has one bad property: a wrong answer is a translation failure. The build stops at the first one, you get a compiler diagnostic instead of a test result, and every other test in the file goes unrun. The xUnit report is empty. You learn that something is wrong, once, and nothing about the rest.

Read more…

An Infix Backtick Operator for C++

I am working on a proposal to let any callable be written between two backticks as an infix binary operator: x `f` y means exactly f(x, y). It is borrowed from Haskell, it desugars to an ordinary call, and I have it working in both Clang and GCC. This post is the announcement and the design tour.

Read more…

Refreshing a Stale Git Subtree

I write my WG21 papers with MPark/WG21, a Pandoc-based framework I vendor into the paper repo as a git subtree. The framework had a major overhaul–the build system split apart, and Pandoc jumped from 2.18 to 3.9–and my copy was 98 commits behind. Worse, the subtree had drifted in two directions at once: real local patches for a TLS-intercepting corporate network, and a pile of pointless autoformatter churn from my own pre-commit hooks. This is how I dragged it back to a verbatim copy of upstream, moved to the new flat.mk include, and pushed every local change back out of the subtree so the next update is a one-liner.

Read more…

Moving Forward With Legacy Encodings

1. Abstract

Reverse-parsing legacy multibyte text encodings — such as Shift_JIS, Big5, or GB18030 — using only local context is an unsolvable problem. Unlike UTF-8, which guarantees \(O(1)\) self-synchronization, legacy encodings have heavily overlapping lead and trail byte ranges. Consequently, even if you begin at a known, valid character boundary, computing the byte-width of the preceding character requires an \(O(N)\) backward scan to the beginning of the string to resolve the parity of the sequence.

The WHATWG decoding algorithms provide no mitigation, as their forward-looking state machines reset completely at every boundary. Robust reverse iteration through these encodings cannot be solved algorithmically in situ; it requires maintaining an external cache of boundary offsets established during a forward pass.

What follows is the story of attempting to find a way out, and why the math forces us to fail.

Read more…

Tailwind, Modus Themes, and the Blog Theming Workflow

I replaced the Foundation 6 theme on this blog with Tailwind CSS. The immediate motivation was a CSS conflict—Foundation's global code and kbd rules bled into org-mode source blocks—but the deeper reason is that Tailwind has the community and documentation that Foundation no longer does.

This post documents the workflow: how the theme is structured, how syntax highlighting CSS connects Emacs to the browser, and what the current configuration choices are.

Read more…

Why Standard Organizations

The question of why C++ is standardized through ISO comes up fairly often. This is what I came up with as an explanation the last time I tried to answer that.

It's a radically oversimplified too long elevator pitch.

Read more…

The Sender Sub-Language

The paper The Sender Sub-Language by Vinnie Falco <vinnie.falco@gmail.com> and Mungo Gill <mungo.gill@me.com> makes extensive use of my work at https://github.com/steve-downey/sender-examples. The code is also the basis for my talk at C++Now 2023, Using the C++ Sender/Receiver Framework: Implement Control Flow for Async Processing. They present the code accurately and fairly, and I am very happy they found it useful in describing and understanding the capabilities of Senders in the framework. There is no higher praise than someone finding your work useful to build upon.

Nonetheless, we come to different overall conclusions about the Sender/Receiver framework.

Read more…