iTranslated by AI
Is the Asterisk Outdated? Pointer Type Notation in Modern C
This article is for Day 11 of the C Advent Calendar 2025.
This article is half a joke and half serious.
Issues with Pointer Type Notation in C
In C, the asterisk * is used to denote pointer types. For example, a pointer to an int is written as int *. However, there are two problems with C's pointer type notation:
Problem 1: Counter-intuitive results when declaring multiple variables.
Since the asterisk attaches to the variable rather than the type, it leads to non-intuitive results when declaring multiple variables. Let's try running the following code:
#include <stdio.h>
#define reveal_type(x) \
printf("%s: %s\n", #x, _Generic((x), int: "int", int *: "int *", int **: "int **"))
int main()
{
int * a, b, c;
reveal_type(a);
reveal_type(b);
reveal_type(c);
}
$ clang -std=c23 ptr1.c
$ ./a.out
a: int *
b: int
c: int
Only a became a pointer type, while b and c became regular int types.
Problem 2: Complex notation for function pointers and pointers to arrays.
While this isn't strictly a problem with pointer types themselves, C's function types also use complex notation, making their combination—function pointers—equally difficult to read.
A function type in C follows this form:
<return type> <name>(<arguments>)
Notice that the arguments come after the name (variable name). For example, a function that returns a function pointer int (*)(int, int) would be:
int (*get_fn_ptr())(int, int);
You might still be able to read this, but what about the type of the standard C signal function?
void (*signal(int sig, void (*func)(int)))(int);
For those not well-versed in C, this might seem quite cryptic.
A typical workaround is to use typedef. An equivalent definition of the signal function above (though with many typedefs) can be written as follows:
typedef void (*sighandler_t)(int);
sighandler_t signal(int sig, sighandler_t func);
Array types suffer from similar issues:
int (*foo[5])[3];
Will typeof be the Savior?
One of the new features in C23 is the typeof operator. The typeof operator takes an expression or a type name and returns a type.
A typical example is its use in macros. For instance, a macro to swap the values of variables can be written like this:
#define swap(a, b) \
do { \
typeof(a) tmp = a; \
a = b; \
b = tmp; \
} while(0)
The typeof operator has long been available as a GNU extension in GCC and Clang.
While the typical use of typeof is to "get the type of an expression" as shown above, the typeof operator can also be given a type name.
By doing this, you can obtain a type name that represents a "pointer type" without using typedef. Let's look at the following code:
#include <stdio.h>
#define reveal_type(x) \
printf("%s: %s\n", #x, _Generic((x), int: "int", int *: "int *", int **: "int **"))
int main()
{
typeof(int *) a, b, c;
reveal_type(a);
reveal_type(b);
reveal_type(c);
}
Let's try compiling this program as C23. The -std=c23 option is available in GCC 14 / Clang 18 and later. In earlier versions, you might be able to enable some C23 features with -std=c2x.
$ clang -std=c23 ptr2.c
$ ./a.out
a: int *
b: int *
c: int *
All variables have now become pointer types. Wonderful.
What would the signal function look like?
typeof(void (int)) *signal(int sig, typeof(void (int)) *func);
Looks okay...? Or maybe not... Some might say "readability has improved," while others might say it hasn't changed much.
Regardless, don't you think the typeof operator holds the potential to solve the problems of type notation in C?
Let's Make It a Macro
Let me introduce the "new notation for pointer types" proposed in this article. First, we define a macro like this:
#define Ptr(T) typeof(typeof(T) *)
Simple, right? Now let's try using it. First, with multiple variables.
#include <stdio.h>
#define Ptr(T) typeof(typeof(T) *)
#define reveal_type(x) \
printf("%s: %s\n", #x, _Generic((x), int: "int", int *: "int *", int **: "int **"))
int main()
{
Ptr(int) a, b, c;
reveal_type(a);
reveal_type(b);
reveal_type(c);
}
$ clang -std=c23 -Wall ptr3.c
$ ./a.out
a: int *
b: int *
c: int *
Looks good. Let's also see how the signal function can be written:
Ptr(void (int)) signal(int, Ptr(void (int)) func);
Compared to using typeof directly, the character count is slightly reduced, making it a bit easier to read, don't you think?
On a Serious Note
Would I actually use the Ptr macro in real-world code? Personally, my answer is no. This is what I meant by "half a joke" at the beginning of the article; I think there are few situations that justify using custom macros to replace basic C syntax.
On the other hand, the reason I wrote "half serious" is that I believe it's useful to keep in the back of your mind that typeof can potentially improve readability when you need to write complex types in C. Though, it might be a bit of a tough sell when compared to just using typedef.
Related
Here are Proposal N2927, which introduced typeof to C23 (note that in the final C23, the other operator became typeof_unqual), and Proposal N3450, which suggests making functions like signal more readable(?) using typeof.
For other new features in C23, please refer to "The contents of the next C standard (C23) seem to have been finalized" (Japanese).
[Addendum] It seems a proposal has been submitted for C2y to extract just the functionality of making type names cleaner (as discussed in this article) from typeof and name it _Typeas: N3759: Add operator _Typeas
Discussion
FYI:2025年2月会合の議事録によると、提案N3450のC2y採択投票が行われ「合意に至らず(not consensus)」となったようです。(今後も進展あるとしたら)古き良き
typedef利用が優勢のようです。