Name n3634, alx-0070r2 - Add operator _Typeas Principles - Enable secure programming Category New operator. Author Alejandro Colomar History r0 (2025-11-14): - Initial draft. r1 (2025-12-02, n3759): - typeas() can be implemented as a macro. r2 (2025-12-09; n3634): - Define _Typeas() as a typeof operator, as suggested by Joseph. Description Certain type-generic macros enhance the (type) safety of functions. For example, there's MALLOC(): #define mallocarray(n, size) reallocarray(NULL, n, size) #define MALLOC(n, T) \ ( \ (typeof(T) *){mallocarray(n, sizeof(T))} \ ) which is used as int *p; p = MALLOC(42, int); where regular malloc(3) calls, depending on coding styles, are usually some of these: A) p = mallocarray(42, sizeof(*p)); B) p = (int *){mallocarray(42, sizeof(int))}; C) p = (int *){malloc(sizeof(int [42]))}; (Or combinations of those. Also, some styles use a cast instead of a compound literal.) The macro has several advantages over regular calls to malloc(3). The problems with the regular function calls are: A) - The type isn't explicit in the call. This makes it impossible to grep(1) for all allocations of a given type. It also makes it slightly less readable for the programmer, which needs to find the declaration of the pointer to check which type it has. - There's nothing that verifies that 'sizeof(*p)' is correctly specified. I tried GCC, Clang, and clang-tidy with a bogus call: p = mallocarray(42, sizeof(p)); and none of them complained about it. B) - The type within sizeof() is de-coupled from the type of 'p'. If the type of 'p' changes, and the programmer forgets to change the type within sizeof(), then the call will request an incorrect size. C) - The issues of B apply to C as well. - The size calculation may overflow size_t, producing a bogus (too low) size request, which would later result in a buffer overflow. All of these are solved through the MALLOC() macro. Problem However, this macro is still imperfect. The macro is defined as: #define MALLOC(n, T) \ ( \ (typeof(T) *){mallocarray(n, sizeof(T))} \ ) Nothing in the macro verifies that T is indeed a type. One could call by accident p = MALLOC(42, 7); and it would be accepted by the compiler. One could consider defining MALLOC() as #define MALLOC(n, T) \ ( \ (T *){mallocarray(n, sizeof(T))} \ ) Such a definition would make sure that T is a type. However, that wouldn't work for certain types. Consider the following code: int (*p)[7]; p = MALLOC(42, int [7]); That would expand to 'int [7] *', which is a syntax error: malloc.c:4:27: error: expected ‘)’ before ‘*’ token 4 | #define MALLOC(n, T) ((T *){mallocarray(n, sizeof(T))}) | ~ ^ malloc.c:11:13: note: in expansion of macro ‘MALLOC’ 11 | p = MALLOC(42, int [7]); | ^~~~~~ It could be solved with a more complex use of typeof(): #define MALLOC(n, T) \ ( \ (typeof(typeof((T){}) *){mallocarray(n, sizeof(T))}\ ) which could be wrapped as #define typeas(T) typeof((T){}) #define MALLOC(n, T) \ ( \ (typeas(T) *){mallocarray(n, sizeof(T))} \ ) This is indeed a valid implementation of _Typeas() as specified in this proposal. To make it easier for projects to use this level of safety, and also to raise awareness of it, it would be useful to provide this operator as part of the compiler. Simple compilers that don't want to grow their source code too much could decide to provide it as a pre-defined macro. A solution would be to add a new operator, inspired by _Alignas(). The operator would produce a type specifier, like typeof(), but it would require that the operand is a type name. The name of the operator would be _Typeas(). The macro MALLOC() could be defined as #define MALLOC(n, T) \ ( \ (_Typeas(T) *){mallocarray(n, sizeof(T))} \ ) Which would have the same guarantees that the current MALLOC() has, plus it would require that T is a type. Proposed wording Based on N3550. 6.4.2 Keywords @@ Syntax, p1 keyword: one of ... _Noreturn + _Typeas 6.5.3.6 Compound literals @@ Constraints, p4 ... - SC typeof(T) ID = { IL }; + SC _Typeas(T) ID = { IL }; ... 6.7.1 Declarations :: General @@ Constraints, p3 EXAMPLE 1 The following are invalid, because the declared tag or enumeration constants are in a nested construct, rather than a declaration specifier of the declaration being of one of the given forms: struct { struct s2 { int x2a; } x2b; }; - typeof (struct s3 { int x3; }); + _Typeas (struct s3 { int x3; }); alignas (struct s4 { int x4; }) int; - typeof (struct s5 *); + _Typeas (struct s5 *); - typeof (enum { E6 }); + _Typeas (enum { E6 }); struct { void (*p)(struct s7 *); }; @@ p15 EXAMPLE 3 As declarations using constexpr are underspecified, the following has implementation-defined behavior because tokens within the declaration declare s which is not an ordinary identifier: - constexpr typeof(struct s *) x = 0; + constexpr _Typeas(struct s *) x = 0; 6.7.3.6 Typeof specifiers @@ Syntax, p1 typeof-specifier: typeof ( typeof-specifier-argument ) typeof_unqual ( typeof-specifier-argument ) + _Typeas ( type-name ) ... @@ p2 The -typeof +typeof, -and -typeof_unqual +typeof_unqual, +and +_Typeas tokens are collectively called the typeof operators. @@ Semantics, p5 ... The typeof +and _Typeas -operator preserves +operators preserve all qualifiers. @@ p12+1 +EXAMPLE 8 + _Typeas(int) + main(void) + { + return 0; + } +is equivalent to this program: + int + main(void) + { + return 0; + } A.2.2 Keywords A.3.2 Declarations J.6.3 Particular identifiers or keywords M.2 Sixth edition Index ## Update.