C syntax is just horrible. It would be better to have something like "ARRAY [100] OF POINTER TO FUNCTION RETURNING POINTER TO INT", if you need it more than once or twice you can declare a type for it.
Still unclear at things like “returning pointer to array of pointer to function …”.
The best shot is to name intermediate types, since they aren’t really intermediate most of the times. Properly structured programs tend to disassemble complex structures to work on them, so you need these names anyway.
That said, C compilers definitely lack -Wunreadable-types flag which must be default.
I was about to say the same thing. If you have an array of widgets used in several places in your program it would be odd not give it a name. Then you can have a pointer to it, etc.
Pointers to arrays are an incredibly rare thing in real-world C programming, let alone functions returning them. They contain the same address as a pointer to the beginning of that array, with the only difference that pointer arithmetic will be done in sizeof(arr) steps, not in element-size steps.
It is a widely accepted practice to typedef pointers to functions.
With pointers, more than 2 levels of indirection is an indication of a badly written program.
In practice, the most complex declarations you should write are "pointer to pointer to x", "array of pointers to x", "array of function pointers returning x". Stuff like "pointer to function returning pointer to pointer to array" is nonsensical most of the time.
All syntax is horrible. Rust is the en vogue tool of the moment and it's objectively harder to read than C. Python used to be clean and now it's sort of a mess. C++, well...
Some languages try to get away with having simple syntax though. Javascript is in this camp, as of course are all the Lisps. And the result is that you end up drowning in a sea of complicated semantics instead, c.f. the famous "wat" video or the fact that every sexpr macro-ized DSL works like its own little butterfly.
You can't win. C is fine. Could you do better if you started from scratch? Sure. Would it last? No.
> Rust is the en vogue tool of the moment and it's objectively harder to read than C
I think you're just used to C syntax. I find Rust syntax much easier to read than C syntax (and that was already true when I didn't know either language well).
Meh. Grandparent who thinks "C is horrible" just isn't used to C. Familiarity breeds facility, that's no surprise. But no, Rust is just bad at this point given all the stuff that's been added and all the historical idioms that were pushed and then abandoned. It's not C++ bad, but it's objectively "worse" than C whose quirks all fit in a tiny K&R paperback.
it's subjective to an extent, but requiring a "reverse spiral rule" (which itself has exceptions) to understand inline types is about as close to objectively bad as it gets.
rust surely has it's issues, but reading types is not one of them. whatever you put inside the <> of a outermost symbol is a straightforward tree parse.
The "reverse spiral rule" is a myth and that website should be shut down. It only adds confusion for the already confused.
C type syntax has a simple guiding principle, there is no type syntax and "declaration follows usage" i.e. normal expression syntax. A few ugly additions that don't cleanly fit in this model were made later for practical reasons, but they don't change the basic idea, which is that there is so little to C type syntax that you can barely see it (which may also be why it's so easy to read once you've grokked it).
If anything is objectively wrong, it's C's grammar, and it's not even close.
When you have an entire Wikipedia article dedicated to how infamously hard your language is to parse, you have no choice but to admit that you've messed up: https://en.wikipedia.org/wiki/Lexer_hack
Having written a C parser, C is not hard to parse. It does not fit some nice model, and if you assume this and don't know about the issues, you might waste your time. It iis still simple to write a C parser.
C itself is not that hard to parse. The "lexer hack" thing is a theoretical impurity but it just means you have to add types to a type table -- and probably open and close scopes -- while parsing. The preprocessor though seems to be pretty annoying.
C++ must be comparatively a lot harder to parse -- C++ syntax is a huge amount of complexity added to a system that wasn't designed with that in mind.
That you link "Dangling else" is curious. The wikipedia page talks about "ambiguity" but in my mind it's no different than the ambiguity in "a + b * c", does it mean "(a + b) * c" or "a + (b * c)"? Well, make a choice and write it down.
Or don't and don't allow unnecessary ambiguity. For * and +, there's a agreed upon order of operations far more universal than a programming language, but it's sensible to use a partial order so nobody is mistaken about whether e.g. logical or and bitshift binds tighter. Similary C counld have just not have allowed omitting the braces around single-line else clauses and would have been a simpler language for it.
> For * and +, there's a agreed upon order of operations
So... we collectively made a choice and wrote it down, then? I think you whooshed on the point. The if/else ambiguity is just a precedence rule, like many others in the expression grammar of every (well, every non-lisp) language that exists.
Needing to specify associativity for operations that have absolutely no reason to be ambiguous in the first place is a flaw of language design. It's needless complexity. Isn't C supposed to be a "simple" language?
You lost me. How does if/else have "no reason to be ambiguous in the first place". It's ambiguous in the actual language from which it was derived. English itself has no syntax for expressing a nested "if" such that you can match the right "else".
> it just means you have to add types to a type table -- and probably open and close scopes -- while parsing
Which is a flaw that other languages don't have to deal with, because they learned from C's mistakes. There's no justifiable reason for a language's grammar to make this sort of thing necessary. C would be better--and simpler--without it.
Did they learn? What about Java or C# then, for example?
I would like to have a better syntax too but it's a tradeoff. I really want to like more algebraic syntaxes like Zig/Odin/Jai but C's terseness and ease of use for the common, simple cases is unmatched unfortunately.
There are several languages which are motivated by providing a better experience than C in C's native domain. Rust is not one of those languages.
Rust has an innovative, but highly opinionated, memory model: the premise is writing high performance memory safe programs, with 'zero cost' abstractions and so on. You might write a program in Rust rather than C in the way you might write a program in Go rather than C: because the application is better modeled in a language which isn't C. For programs which pertain to the domain where C should be considered on its merits, if you try to write them in Rust, you may as well start each module with an unsafe block.
But no, C is not fine. It has a stunning amount of own-goals making it pointlessly difficult to write a program with a fine-grained memory policy that is correct. C also doesn't have a syntax at all until you've run CPP, and then you have a context-sensitive parse. Neither of these are mere technical formalities, they actively inhibit understanding a C program.
> For programs which pertain to the domain where C should be considered on its merits, if you try to write them in Rust, you may as well start each module with an unsafe block.
This is often said, but does not reflect reality. At this point, we have tons of bare-metal and OS-level Rust code existing in the world, and it still has very little unsafe overall.
Granted, that was put poorly, let me try again. What I said doesn't in fact have the implication you took from it, but I didn't explain myself particularly well.
There are many programs one can write in C, or Zig, or some other manual-transmission language, which, if you wrote them in Rust, would just be wrapped in a big unsafe block. That isn't because those programs have memory errors in them, it's because they have a memory policy which safe Rust doesn't support.
Examples include certain garbage collection algorithms, and embedded programs where all memory is pre-allocated and references shared somewhat promiscuously. It's possible to write analogous programs in safe Rust, but not the same program. The space of programs which are both correct, and not possible in safe Rust, is infinite. They don't come with compiler guarantees, so other support is needed. CompCert is a good example of an approach to that which isn't Rust's.
Keeping in mind that I'm referring to specific programs, not "a program which solves the problem domain". Otherwise we could just say that since you can write a program which solves the problem domain in Python, there's no use for Rust. That wouldn't lead to a productive conversation, would it.
People still choose C over Rust, even knowing what Rust is. This isn't the early days of the evangelism strike force, Steve. The above is why.
Sometimes they might be better off writing the analogous program in Rust, or seeing if they can get the program they're trying to write with limited use of unsafe blocks. Sometimes not. I know you've put a lot of work into your hammer over the years, but it's never going to be the only tool in the box.
Languages, and I'm thinking of Zig here, which make it easier to correctly implement a memory policy which doesn't happen to be Rust's, should be applauded, and not called "a massive step backwards for the industry <pouty sad face>". Like it or not, they're working in the same domain as Rust, based on different principles.
But yes, there is an infinitely large class of programs which can be written in the embedded and OS spaces, in Rust, with very little unsafe overall. I didn't intend to imply otherwise.
I agree it's annoying (really bad for tooling), and what's more annoying is that for a human who understands how it works, there is no syntax as easy to read and write as C declaration syntax. It's terse and intuitive, not adding new type syntax but reusing what already exists as expression syntax.
Treat them like ordinary expressions around an identifier. At the deepest level is the identifer, then some operators around it with a certain precedence, and finally, on the left, a list of type specifiers indicating the final type of the expression when all the operators are applied.
Those operators are the same ones you would use in an expression to use your declared object. They are the unary dereferencing operator, the array subscription operator, the function call operator and the parentheses for overcoming default precedence.
It is a bit sad that an entire blog post is required to explain type declarations in a language as common as C. It is even worse that C++ stuck with it. It more atrocious still that C decided it was good syntax to attach operator* to the name instead of the type to justify this crazy business, which thankfully C++ reversed a little with `auto` and West const.
Imagine how much easier that last example would be with a trailing return type, Ptr<> and Array<> parametrised types, and lambda-esque function objects instead of function pointers:
char * (*(**foo[][8])())[]
versus
let foo: Array<Array<Ptr<() -> Ptr<Arr<Ptr<Char>>>>, 8>> = ...
It's immediately clear what the top-level type of `foo` is: some parametrised `Array`. We can do better, though.
In this hypothetical language, one would probably dispense with 'pointer to char' and 'pointer to array' and replace them with view/range/span types, eliminating a layer of type parametrisation and many classes of bugs:
let foo: Span<Array<Ptr<() -> Ptr<Arr<StringView>>>, 8>> = ...
Though I am dumbfounded by why there needs to be a 'pointer to array' in the function return type, and a 'pointer to function pointer'. I assume these are unknown arrays themselves, which means...
let foo: Span<Array<Span<() -> Span<Arr<StringView>>>, 8>> = ...
A perfect language would probably have multi-dimensional spans, which would eliminate the two `Span<Array<>>` and replace with some `MultiSpan<>`.
Addendum: https://cdecl.org/ is a very useful and very self-aware resource: 'C gibberish ↔ English'.
In C++, you can do much of what you want with std::array, std::mdspan... and you could also create a Ptr<T> with "using" to define a type alias for T* -- see [0]. Of course the default syntax for pointers is T* and that's not going to change, so it'll be an uphill battle to keep consistent in any codebase. (I guess systematic use of a rule in clang-tidy or such could help with that.)
Indeed, and there's `std::function` (albeit not being a zero-cost abstraction; I believe `std::move_only_function` and `std::copyable_function` in C++23 and C++26 respectively are replacements) and lambda statements. But C++ decided the bracket salad `[](){}` was the best way to write lambdas, and I'll never forgive the standard for it.
Any sort of type erasure on functions will have a non-zero cost. That's why the only zero-cost abstraction is having a template parameter (such as remove_if), but these come with a code size cost instead.
West const is a travesty given that either side, or both, of a pointer can be `const`. And with refs I prefer to see that something is a `const &` in one place instead of scanning 3 km of template argument list to find that out.
I've never seen anything that complex in my experience, and it would be considered bad code pretty much everywhere.
Rule #1 of programming is to make code easy to understand and self-documenting. This should be a sequence of typedefs with self-explanatory names followed by a function call taking one pointer or type and returning another.
Still, C syntax is 100 times easier to remember than things like templating, smart pointers, dozen flavours of initialyzers in C++, which is used continuously as well.
The lack of colons is also hard for the parser. The recursive-descent code for parsing a Go parameter declaration is quite tricky and got even trickier after they introduced generic types.
The types stand out more with colons, certainly, but I don't find the former example any harder to read. It helps that types can only show up in certain places. Readability does suffer when multiple consecutive parameters use the same type and it's elided on all but the last, though. That's a quirk unique to Go that I haven't seen on any language using colons to specify types.
Ada and Pascal are both languages which allow multiple consecutive parameters with the same type and also use `name: type` syntax. I agree, even when using Ada/Pascal with colons its difficult to mentally parse.
It's subjective, but personally I've found multiple-declarations always harm readability.
It's especially terrible in C and C++, where you need to be on your guard for whether all the variables are being declared with the same type or whether some are pointers. Even in languages without this quirk though, I find it still slows my comprehension.
Unlike readability, conciseness of code isn't a virtue in and of itself. I see little upside to multiple-declarations, but clear downside.
Language trivia: the D language does not follow C's rule, instead it guarantees all variables in a multiple-declaration statement all have identical type:
IMHO the way comma behaves when declaring multiple variables of the same type was a mistake from day one. The sort of thing that only sticks around because it would break too much existing code to fix it. If I were in charge I'd declare the syntax deprecated in the next version of C and introduce a new character, maybe | that would do the same thing except it would propagate the pointer as you would expect.
In my code I never use it. Every variable gets its own line. You might think this would blow up the declarations, but in practice it's not a problem, especially in modern C where you can move declarations down to where they're used instead of stuffing them all up at the top of the function.
The only "type" thing about the declaration above is "int", and that does get applied to all the comma-separated expressions -- so in that way the comma acts totally reasonable.
People think that "int *x;" means "declare x as a pointer-to-int" but really it means "declare x such that *x (it's a code expression) is of type int." So it's a really roundabout way to say that x is a "pointer to x" while that concept of "pointer to" isn't even in the language syntax.
It's not inconsistent. There are two things in a declaration: a type (here int) and an expression (here *x). If you add commas you can have multiple of the latter. That's it. Stop thinking of the star as part of the type, it isn't. Leave it with the expression to the right, and thinks make sense .
The idea that the * isn't part of the type has always felt completely wrong to me. The entire concept of an expression modifying the type doesn't provide any benefit that I can see. The only thing the expression should be doing is giving you the chance to assign the name to the new variable you're creating. Once you define the type, which might be a pointer or not, it should be fixed.
I get that this is a pretty fundamental disagreement between me and Knuth & Richie, but I think history has shown that this idea of splitting exactly one aspect of the type (if the variable is direct or a pointer) was a mistake. It's been the cause of a lot of bugs and the cases where you want to define both direct and indirect types on the same line basically don't exist. For clarity you should split those definitions up regardless of what the syntax allows.
There are basically no cases where you should be writing the following code even if it is technically correct:
I never use comma, I always define one variable per line. Much easier to edit and read. However, the way it works is certainly not inconsistent.
As to your other concerns, it makes perfect sense.
int WHATEVER;
read as "WHATEVER is an int" and you can work backwards from there. There is no more type syntax than that. The thing is, it's much easier to look at stuff this way since otherwise you'll have to shift back and forth between 2 different syntaxes (types and expressions) all the time.
I've been looking a lot for a better more algebraic syntax too. All syntaxes I've found have lost on two axes _in the common and simple cases_
- low cognitive overhead
- terseness
Yes, the principle of declaration follows usage is already stretched to its limit, and C++ has certainly overstretched it. But still for the bread-and-butter use cases, the "better" syntaxes seem to always lose on those 2 axes.
I only declare multiple variables in one line if it is a "simple" type, without pointers, arrays, functions, etc.
Also, a code such as:
int*x=y;
will define the initial value of x and not of *x. For this reason, a space before the asterisk can be confusing in this way. And, for the reasons described in another comment, putting the space after the asterisk is also confusing. So, I find it clearer to omit both spaces.
When I require more complicated types, I will generally use typedef (or typeof) instead of having to deal with the confusing syntax of types in C.
The rest of your post is perfect and objectively (subjectively) correct.
While I fortunately knew that `int* x, y` means `y` is not a pointer, I always assumed it was some kind of capricious oversight of the compiler. I had no idea the rationale was "the expression `*x` declares `x` to be a pointer to the type west of it". That strikes me as so weird! XD
'Pointer to something' should have been a parametrised type rather than C's weird syntax of overloading operator*. And I won't accept the argument that C was too old to have parametrised types: ML, Lisp, and many other functional-first languages developed alongside C and had them. We've just stuck with C because it got popular because of UNIX and then Linux.
I avoid raw pointers whenever I can when I write C++, and I yearn for a language that exposes pointers with a rich and expressive type system instead of making them glorified 64-bit unsigned ints. Pointers ought to be one-dimensional unsigned parametrised affine space types[1] (note: not affine types, which are completely different[2]), with several implications:
- comparing pointers to different types should be invalid
- subtracting pointers to the same type should return a signed 'pointer difference' type
- adding pointers to the same type should be invalid
- Get the address contained by a pointer with Ptr<T>::address()
Programming languages are a huge waste of time. It's a domain with lots of opportunities for bike-shedding, and then for hard-core R&D too. But programming is still engineering, simple & easy that gets the job done wins over needlessly complex. And yes, inertia is real too, but if a programming language was dramatically better at what C does best, I wouldn't still start new code in it.
When you're doing systems programming, pointers being "glorified" 64-bit ints is pretty much what you need. When your requirements hit reality, all the nice abstractions break down quite easily.
> I avoid raw pointers whenever I can when I write C++
Empirically, this is the path to madness.
> - comparing pointers to different types should be invalid
It produces a warning on my C compiler and an error on C++ compiler. Note that there _can_ be pointers containing equal addresses with different types -- a struct and its first member have the same address.
> - adding pointers to the same type should be invalid
I think it is, or what exactly do you mean?
> - subtracting pointers to the same type should return a signed 'pointer difference' type
it does, that type is called ptrdiff_t, which is a signed integer type.
> - Get the address contained by a pointer with Ptr<T>::address()
But you didn't want them to be glorified 64-bit ints? Well, because of pointer arithmetic, which is a practical necessity, they kind of are, but then again they're not because C tries to abstract from that. You can kinda unwrap the pointer abstraction by casting to the (optional) uintptr_t type.
The thing is, not all machines necessarily have one flat address space that contains everything from functions to all data. Pointer representation isn't necessarily uniform -- e.g., you can have a bigger type for void-pointers that contain type information too, this way you could do run-time type checking.
Of course you can read it. That was just an example to show that C declarations can't always be decomposed into a left part which is the type and a right one which is the variable. It works on the simple case of a pointer, because * is a prefix operator, but it fails on arrays, because [] is postfix.
int * <---> foo;
int [] <---> foo; // ugh no!
The latter would work if C had been designed differently, with operations defined on types. But it was designed with operations defined on values, not on types. There's not a way to express "foo is a pointer to int", you must say "when you dereference foo you get an int" (in fact, * means dereference, not pointer).
Imagine if English had not a word for "son". To say Bob is Adam's son you would say "Bob's father is Adam".
To tell the truth, C does have the dual of * that is &. If operations on types were defined (and they are not!), then we could have a syntax like this:
&int foo;
In any case, we wouldn't solve the issue with arrays, because, while [] is the operator that, given an array, returns one element of it, its inverse operator doesn't exist.
It's not difficult to imagine a different language with a rich set of operators defined for types. We could have &T for "pointer to type T", Tn (note the infix , as multiplication) for "array of n elements of type T", and so on. Then declarations could be easier:
&int ptr;
float*3 rgb;
&int*8 foo; // array of 8 ptr
&(int*8) bar; // ptr to array
See how we are using full expressions including parentheses with types?
At this point, you would also move the variable to the left and the type to the right, and maybe add a colon for clarity:
while just about anything is better than the original, we could certainly do better. I am curious how you expected to handle function pointer syntax, as that is the usual bear of C typing.
let's aim to make the types unambiguous, simple to read and write
all C types can take at most one parameterizing type, so prefixing works unambiguously.
force const and volatile to be exclusively prefixing as well, sorting the weirdness trailing consts would allow.
() can represent a function-pointer, with its return type following
Oh yes, introducing expressions on types means defining a new, specific syntax for each construct.
However, I strongly believe well-known symbols should keep their properties independently of whether they are used in type expression or in value expressions. So, for example, [] is already a postfix operator: it would be confusing if it was sometimes a postfix sometimes a prefix. And the unary * is already used to dereference, it would be weird if sometimes it was used to take a reference instead (for that there's already the unary &).
And don't forget the precedence: you shouldn't redefine it. So &int[] would mean &(int[]) not (&int)[].
And since we're talking of a hypothetical new language, for function types I'd choose to re-use an existing operator although for a completely different meaning: ->
Raw pointers "are" a type too. It's just that there isn't really syntax to describe the type, at least in the original declaration syntax.
Note that structs and typedefs that you define are proper type names, you can switch out "int" for them. You can easily "typedef int *intPtr;" and then merrily go "intPtr myptr".
(I think cast type-specs might be a bit of an exception here were the rule breaks a bit -- because they don't "use" a name in the cast "expression". I don't intimately understand how those work. The whole thing is an idea that was stretched to the limits, especially with C++ where consistency has pretty much been given up).
I can only assume he removed them, not for brevity, but as a compromise for the sake of C programmers to only ask them to learn one new thing rather than two.
I think I've been writing C for well over 10 years now and this year was finally the one where I forced myself to remember function pointer declarations.
I don't even know why it took me so long to remember such a simple thing, but possibly writing "c function pointer" into my favorite search engine and hitting enter was faster than thinking about it for a moment.
But maybe also because I changed up my general programming approach a bit and try to think a bit longer before hitting up a search engine. Also forced myself to read more reference sheets than stackoverflow or chatgpt. And I must say, this made me a lot more comfortable, because I understand more. Can recommend to go through the struggle/10
Function pointers are easy if you typedef the function type.
What a pointer to function (int) returning int?
The difficult way:
int (*p)(int);
The easy way:
typedef int Func(int);
Func *p;
The typedef reads just like a normal function declaration except for the name is the type name rather than the function symbol name. Just like a function declaration, you can add more documentation by giving the parameters names. e.g.:
// used for some hypothetical allocator API
typedef void *Alloc(size_t size);
Similar trick can be used for pointers to arrays (just typedef the array type). Although in practice pointers to arrays just make for awful code.
Unfortunately I was astounded, horrified, and simultaneously pleased to discover that I was immediately able to decipher the "hard" (huh ?????) example.
Although if I was writing this myself, I would split it up like so:
Which is equivalent and much easier to read. (Although still not trivial. I have no idea what purpose this type really serves. The type names are placeholders as a result.)
C type declarations become particularly tricky specifically when people try to directly declare (or typedef) pointers to functions/arrays without an intermediate typedef for the function/array type.
So if you make it a rule to typedef array types before creating pointers to them (although this usually makes for some rather unreadable code so maybe try avoiding it in general) and to typedef function types before creating pointers to them, you will usually have much more readable code.
As a bonus, you can then use the function typedef to provide an early type-check for a callback function (even if it's a callback your code doesn't directly use). E.g.:
typedef int Func(int a);
Func foo;
int foo(int a)
{
return a * 2;
}
C type declarations (and array decays, for that matter) seem to be baggage from B.
In B the only type is the machine word, which can perform arithmetic and be dereferenced. Arrays the only special kind of declaration and are declared like
auto foo[10]
Where auto is a storage class specifier like extern or static.
In that world, having declaration follow usage makes perfect sense. There's no "other" kinds of data to worry about, the operators you can apply to an expression describes its type exactly.
As usual the worst mistake is Hoare's Billion Dollar Mistake (null). But yes obviously arrays shouldn't decay.
The "fix" would be to not have arrays decay. I believe C++ could probably get there if it had taken Epochs, I further believe taking Epochs (P1881) when it was proposed would have been extremely disruptive but perhaps possible and worth attempting. I do not believe it's still possible, the moment passed and with it, in my view, any hope of salvaging C++.
Without Epochs, any and every such change to C++ is similarly disruptive and that's too expensive so more or less nothing gets fixed, instead layers of kludges must be added. That's how Foonathan ends up with: class foo final trivially_relocatable namespace() { … };
The change I proposed is not disruptive in any way.
As for the billion dollar mistake, a null pointer reference results in a seg fault. But the array decay results in buffer overflows, the #1 problem in shipped C code, and the buffer overflows result in memory corruption. Memory corruption is much, much, much worse than a seg fault. For example, buffer overflows are exploitable by malware. Seg faults are not.
I am baffled by the lack of interest in incorporating the proposal into C. Instead, C23 gets Unicode normalized identifiers, a complex and pointless feature that is easily achieved by other means, if one really really wants it.
Your post is great and provides an elegant solution to the syntactic case for parameters, but it also lacks almost all detail on how this would work in practice. Say I’m writing the `void food(char a[..])` function. What are the semantics of this fat pointer type, exactly? How do I access the size? What is the ABI around this like? How is the size set on a fat pointer? It just requires so much extra stuff to get it into proposal shape.
null is not a problem and is arguably useful. Look at how zig does it, extremely safe. The problem with most languages is that null is unguarded, and/or generally emittable as part of any given type.
I must say that this "billion dollar mistake" about nulls really baffles me. What's the mistake?
We invent new languages almost daily and none of them have ever resolved the problem. I mean, when you start with a pointer, it's uninitialized. Setting it to null is much better than having it point somewhere, where there might be incorrectly interpreted data.
So, null. We have Java, suffering the same problem, Golang suffering the same problem and Rust, where you have the same problem if you call unwrap, without checking that all code paths ensure you have the value populated.
Seems to me this is an inherent problem of using an uninitialized value... so, what is the mistake?
> I must say that this "billion dollar mistake" about nulls really baffles me. What's the mistake?
The mistake is that Nothing isn't a kind of thing. It's a categorical mistake, enshrined in many programming languages because it was easy and they didn't realise it was a bad idea, that's what Tony's talk is about - he's the one who made the mistake originally.
> I mean, when you start with a pointer, it's uninitialized
You're talking about a language implementation detail. Indeed I'd say plainly, a language defect, particularly notable in C (and so C++) where it continues to cause problems.
> We have Java, suffering the same problem
That's correct, all non-primitive Java objects can be null instead.
> Golang suffering the same problem
That's correct, Go chooses "make representable states legal" rather than "make illegal states unrepresentable" and so every type must have a legal "zero" state.
> and Rust, where you have the same problem if you call unwrap,
Nope. Try it. Call "unwrap" on a Vec, or a HashMap, or a File. You can't, there is no such method, it's a method on Option.
> Seems to me this is an inherent problem of using an uninitialized value... so, what is the mistake?
Goodness no. An actual uninitialized value by default would be a design mistake. They're currently trying to fix this mistake for C++ 26. If for performance reasons you need to delay initialization you need a trick like Rust's MaybeUninit<T> wrapper. You certainly can't paper over it with null.
It doesn't seem that bad to me. You can easily understand it by reading a short article, and it makes sense in a basic CS 101 mental model of computing.
$ sudo apt install cdecl
[…]
$ cdecl
Type `help' or `?' for help
cdecl> explain char *(*(**foo[][8])())[];
declare foo as array of array 8 of pointer to pointer to function returning pointer to array of pointer to char
cdecl>
This is an array of result-defined size, of arrays of eight pointers to fn pointers, which returns a many-item pointer to null-terminated arrays of u8. Null terminated because we're going to assume that the C data structure is properly null-terminated...
Still an insane data structure, and I'm still learning Zig, so I can't promise this is precisely equivalent to the C, I got as close as I could and am unwilling to try and compile, let alone build, such a type.
No one would actually do this unless they needed `extern` compatibility. Ignoring the fact (pointed out in the Fine Article) that no one would use anything vaguely like this, a more Zig native version might be
const foo: [_][8]**fn() [][]const u8
Which returns a slice of slices of immutable u8s, these being fat pointers which include their length.
Why do I remember The C Programming Language book by K&R explaining how to write a version of cdecl? I just checked the second edition of the book and my memory seems to be wrong.
It's not listed as cdecl and (therefore?) not findable in the index, but in chapter 5 - Pointers and Arrays - the book presents the programs dcl and undcl that translate between C declarations and English renderings of them.
It's fun stuff of translating C declarations to English, given English uses its special order for possessive and verb object order. If you use Japanese, you can write the Japanese from left to write in order, while reading the C declarations from outside in.
I wonder if it would have been possible for C to have been less pointer heavy without killing performance on those old systems.
Could we have done without pointer arithmetic? Could we have hidden the pointer part of function pointers and made them syntactically just like Python or JS functions?
Did arrays as raw blocks of memory ever need to exist?
I remember when I first learned C, I didn't really see the problem, I thought it was a perfectly fine language, until I learned Python/JS/C++/Dart and saw how nice high level features and package management is.
But now I wonder how I ever used it at all without complaining!
133 comments
[ 3.3 ms ] story [ 219 ms ] threadPlus the fact that arrays in type names sometimes act as pointers and sometimes not.
The best shot is to name intermediate types, since they aren’t really intermediate most of the times. Properly structured programs tend to disassemble complex structures to work on them, so you need these names anyway.
That said, C compilers definitely lack -Wunreadable-types flag which must be default.
It is a widely accepted practice to typedef pointers to functions.
With pointers, more than 2 levels of indirection is an indication of a badly written program.
In practice, the most complex declarations you should write are "pointer to pointer to x", "array of pointers to x", "array of function pointers returning x". Stuff like "pointer to function returning pointer to pointer to array" is nonsensical most of the time.
All syntax is horrible. Rust is the en vogue tool of the moment and it's objectively harder to read than C. Python used to be clean and now it's sort of a mess. C++, well...
Some languages try to get away with having simple syntax though. Javascript is in this camp, as of course are all the Lisps. And the result is that you end up drowning in a sea of complicated semantics instead, c.f. the famous "wat" video or the fact that every sexpr macro-ized DSL works like its own little butterfly.
You can't win. C is fine. Could you do better if you started from scratch? Sure. Would it last? No.
I think you're just used to C syntax. I find Rust syntax much easier to read than C syntax (and that was already true when I didn't know either language well).
rust surely has it's issues, but reading types is not one of them. whatever you put inside the <> of a outermost symbol is a straightforward tree parse.
C type syntax has a simple guiding principle, there is no type syntax and "declaration follows usage" i.e. normal expression syntax. A few ugly additions that don't cleanly fit in this model were made later for practical reasons, but they don't change the basic idea, which is that there is so little to C type syntax that you can barely see it (which may also be why it's so easy to read once you've grokked it).
When you have an entire Wikipedia article dedicated to how infamously hard your language is to parse, you have no choice but to admit that you've messed up: https://en.wikipedia.org/wiki/Lexer_hack
And let's not forget: https://en.wikipedia.org/wiki/Dangling_else
C++ must be comparatively a lot harder to parse -- C++ syntax is a huge amount of complexity added to a system that wasn't designed with that in mind.
That you link "Dangling else" is curious. The wikipedia page talks about "ambiguity" but in my mind it's no different than the ambiguity in "a + b * c", does it mean "(a + b) * c" or "a + (b * c)"? Well, make a choice and write it down.
Or don't and don't allow unnecessary ambiguity. For * and +, there's a agreed upon order of operations far more universal than a programming language, but it's sensible to use a partial order so nobody is mistaken about whether e.g. logical or and bitshift binds tighter. Similary C counld have just not have allowed omitting the braces around single-line else clauses and would have been a simpler language for it.
So... we collectively made a choice and wrote it down, then? I think you whooshed on the point. The if/else ambiguity is just a precedence rule, like many others in the expression grammar of every (well, every non-lisp) language that exists.
Which is a flaw that other languages don't have to deal with, because they learned from C's mistakes. There's no justifiable reason for a language's grammar to make this sort of thing necessary. C would be better--and simpler--without it.
I would like to have a better syntax too but it's a tradeoff. I really want to like more algebraic syntaxes like Zig/Odin/Jai but C's terseness and ease of use for the common, simple cases is unmatched unfortunately.
If this is "objectively" true please link the study which was able to somehow measure that.
That said, C's syntactic missteps appear to be accidents, while Rust delights itself in being ugly.
Rust has an innovative, but highly opinionated, memory model: the premise is writing high performance memory safe programs, with 'zero cost' abstractions and so on. You might write a program in Rust rather than C in the way you might write a program in Go rather than C: because the application is better modeled in a language which isn't C. For programs which pertain to the domain where C should be considered on its merits, if you try to write them in Rust, you may as well start each module with an unsafe block.
But no, C is not fine. It has a stunning amount of own-goals making it pointlessly difficult to write a program with a fine-grained memory policy that is correct. C also doesn't have a syntax at all until you've run CPP, and then you have a context-sensitive parse. Neither of these are mere technical formalities, they actively inhibit understanding a C program.
This is often said, but does not reflect reality. At this point, we have tons of bare-metal and OS-level Rust code existing in the world, and it still has very little unsafe overall.
There are many programs one can write in C, or Zig, or some other manual-transmission language, which, if you wrote them in Rust, would just be wrapped in a big unsafe block. That isn't because those programs have memory errors in them, it's because they have a memory policy which safe Rust doesn't support.
Examples include certain garbage collection algorithms, and embedded programs where all memory is pre-allocated and references shared somewhat promiscuously. It's possible to write analogous programs in safe Rust, but not the same program. The space of programs which are both correct, and not possible in safe Rust, is infinite. They don't come with compiler guarantees, so other support is needed. CompCert is a good example of an approach to that which isn't Rust's.
Keeping in mind that I'm referring to specific programs, not "a program which solves the problem domain". Otherwise we could just say that since you can write a program which solves the problem domain in Python, there's no use for Rust. That wouldn't lead to a productive conversation, would it.
People still choose C over Rust, even knowing what Rust is. This isn't the early days of the evangelism strike force, Steve. The above is why.
Sometimes they might be better off writing the analogous program in Rust, or seeing if they can get the program they're trying to write with limited use of unsafe blocks. Sometimes not. I know you've put a lot of work into your hammer over the years, but it's never going to be the only tool in the box.
Languages, and I'm thinking of Zig here, which make it easier to correctly implement a memory policy which doesn't happen to be Rust's, should be applauded, and not called "a massive step backwards for the industry <pouty sad face>". Like it or not, they're working in the same domain as Rust, based on different principles.
But yes, there is an infinitely large class of programs which can be written in the embedded and OS spaces, in Rust, with very little unsafe overall. I didn't intend to imply otherwise.
Those operators are the same ones you would use in an expression to use your declared object. They are the unary dereferencing operator, the array subscription operator, the function call operator and the parentheses for overcoming default precedence.
Imagine how much easier that last example would be with a trailing return type, Ptr<> and Array<> parametrised types, and lambda-esque function objects instead of function pointers:
versus It's immediately clear what the top-level type of `foo` is: some parametrised `Array`. We can do better, though.In this hypothetical language, one would probably dispense with 'pointer to char' and 'pointer to array' and replace them with view/range/span types, eliminating a layer of type parametrisation and many classes of bugs:
Though I am dumbfounded by why there needs to be a 'pointer to array' in the function return type, and a 'pointer to function pointer'. I assume these are unknown arrays themselves, which means... A perfect language would probably have multi-dimensional spans, which would eliminate the two `Span<Array<>>` and replace with some `MultiSpan<>`.Addendum: https://cdecl.org/ is a very useful and very self-aware resource: 'C gibberish ↔ English'.
[0] https://en.cppreference.com/w/cpp/language/type_alias
(Also funny, multi-dimensional spans are coming in the form of TensorSpan<T>)
Array<Ptr<(Int, String) -> Bool>>
Should be:
Array<OnClickCallback>
Applying that to C code keeps the insane decls in check, especially when using function pointer types
Rule #1 of programming is to make code easy to understand and self-documenting. This should be a sequence of typedefs with self-explanatory names followed by a function call taking one pointer or type and returning another.
This was the biggest mistake IMO. Go’s declaration syntax needs colons, it would have been 100x more readable.
Compare:
To: The colons would have made it so much more clear to read for me.It's especially terrible in C and C++, where you need to be on your guard for whether all the variables are being declared with the same type or whether some are pointers. Even in languages without this quirk though, I find it still slows my comprehension.
Unlike readability, conciseness of code isn't a virtue in and of itself. I see little upside to multiple-declarations, but clear downside.
Language trivia: the D language does not follow C's rule, instead it guarantees all variables in a multiple-declaration statement all have identical type:
https://dlang.org/spec/declaration.html#declaring-multiple-s...So instead of this:
You’d have this instead:That makes as much sense as `int *i`, when `int* i` is obviously the correct (though heterodox) way.
if you dont have space for that one line, switch back to punchcards
In my code I never use it. Every variable gets its own line. You might think this would blow up the declarations, but in practice it's not a problem, especially in modern C where you can move declarations down to where they're used instead of stuffing them all up at the top of the function.
People think that "int *x;" means "declare x as a pointer-to-int" but really it means "declare x such that *x (it's a code expression) is of type int." So it's a really roundabout way to say that x is a "pointer to x" while that concept of "pointer to" isn't even in the language syntax.
It's why I always cringe when I see
as opposed to There is a reason why K&R do it that way.I get that this is a pretty fundamental disagreement between me and Knuth & Richie, but I think history has shown that this idea of splitting exactly one aspect of the type (if the variable is direct or a pointer) was a mistake. It's been the cause of a lot of bugs and the cases where you want to define both direct and indirect types on the same line basically don't exist. For clarity you should split those definitions up regardless of what the syntax allows.
There are basically no cases where you should be writing the following code even if it is technically correct:
As to your other concerns, it makes perfect sense.
read as "WHATEVER is an int" and you can work backwards from there. There is no more type syntax than that. The thing is, it's much easier to look at stuff this way since otherwise you'll have to shift back and forth between 2 different syntaxes (types and expressions) all the time.I've been looking a lot for a better more algebraic syntax too. All syntaxes I've found have lost on two axes _in the common and simple cases_
- low cognitive overhead
- terseness
Yes, the principle of declaration follows usage is already stretched to its limit, and C++ has certainly overstretched it. But still for the bread-and-butter use cases, the "better" syntaxes seem to always lose on those 2 axes.
Also, a code such as:
will define the initial value of x and not of *x. For this reason, a space before the asterisk can be confusing in this way. And, for the reasons described in another comment, putting the space after the asterisk is also confusing. So, I find it clearer to omit both spaces.When I require more complicated types, I will generally use typedef (or typeof) instead of having to deal with the confusing syntax of types in C.
The rest of your post is perfect and objectively (subjectively) correct.
While I fortunately knew that `int* x, y` means `y` is not a pointer, I always assumed it was some kind of capricious oversight of the compiler. I had no idea the rationale was "the expression `*x` declares `x` to be a pointer to the type west of it". That strikes me as so weird! XD
This was and remains a bad decision, full stop.
'Pointer to something' should have been a parametrised type rather than C's weird syntax of overloading operator*. And I won't accept the argument that C was too old to have parametrised types: ML, Lisp, and many other functional-first languages developed alongside C and had them. We've just stuck with C because it got popular because of UNIX and then Linux.
I avoid raw pointers whenever I can when I write C++, and I yearn for a language that exposes pointers with a rich and expressive type system instead of making them glorified 64-bit unsigned ints. Pointers ought to be one-dimensional unsigned parametrised affine space types[1] (note: not affine types, which are completely different[2]), with several implications:
- comparing pointers to different types should be invalid
- subtracting pointers to the same type should return a signed 'pointer difference' type
- adding pointers to the same type should be invalid
- Get the address contained by a pointer with Ptr<T>::address()
[1]: http://videocortex.io/2018/Affine-Space-Types/
[2]: https://en.wikipedia.org/wiki/Substructural_type_system#Affi...
Programming languages are a huge waste of time. It's a domain with lots of opportunities for bike-shedding, and then for hard-core R&D too. But programming is still engineering, simple & easy that gets the job done wins over needlessly complex. And yes, inertia is real too, but if a programming language was dramatically better at what C does best, I wouldn't still start new code in it.
When you're doing systems programming, pointers being "glorified" 64-bit ints is pretty much what you need. When your requirements hit reality, all the nice abstractions break down quite easily.
> I avoid raw pointers whenever I can when I write C++
Empirically, this is the path to madness.
> - comparing pointers to different types should be invalid
It produces a warning on my C compiler and an error on C++ compiler. Note that there _can_ be pointers containing equal addresses with different types -- a struct and its first member have the same address.
> - adding pointers to the same type should be invalid
I think it is, or what exactly do you mean?
> - subtracting pointers to the same type should return a signed 'pointer difference' type
it does, that type is called ptrdiff_t, which is a signed integer type.
> - Get the address contained by a pointer with Ptr<T>::address()
But you didn't want them to be glorified 64-bit ints? Well, because of pointer arithmetic, which is a practical necessity, they kind of are, but then again they're not because C tries to abstract from that. You can kinda unwrap the pointer abstraction by casting to the (optional) uintptr_t type.
The thing is, not all machines necessarily have one flat address space that contains everything from functions to all data. Pointer representation isn't necessarily uniform -- e.g., you can have a bigger type for void-pointers that contain type information too, this way you could do run-time type checking.
I read it like 'IntegerPointer x' or 'x is an integerPointer' or 'x is a pointer to an integer'
It rhymes with the following : 'MilkGlass m' reads as:
'm is a MilkGlass' or 'm is a Glass of Milk'
Now do a pointer to an array of 200 ints.
'a is of type one hundred intPointers' should, to be more consistent, be written as (int*)[100] a;
I'm not even sure whether that works, never tried it.
The fact that arrays are automatically pointers is very confusing for beginners, especially multi dimension arrays are brainbenders in that respect.
Imagine if English had not a word for "son". To say Bob is Adam's son you would say "Bob's father is Adam".
To tell the truth, C does have the dual of * that is &. If operations on types were defined (and they are not!), then we could have a syntax like this:
In any case, we wouldn't solve the issue with arrays, because, while [] is the operator that, given an array, returns one element of it, its inverse operator doesn't exist.It's not difficult to imagine a different language with a rich set of operators defined for types. We could have &T for "pointer to type T", Tn (note the infix , as multiplication) for "array of n elements of type T", and so on. Then declarations could be easier:
See how we are using full expressions including parentheses with types?At this point, you would also move the variable to the left and the type to the right, and maybe add a colon for clarity:
But this is another language, not C.let's aim to make the types unambiguous, simple to read and write
all C types can take at most one parameterizing type, so prefixing works unambiguously.
force const and volatile to be exclusively prefixing as well, sorting the weirdness trailing consts would allow.
() can represent a function-pointer, with its return type following
your examples would be
that would leave vs I couldn't have written that second one without https://cdecl.org/Of course, this simple prefixing is also exactly what Go did to improve the types, with some syntactic sugar to help the reader along.
However, I strongly believe well-known symbols should keep their properties independently of whether they are used in type expression or in value expressions. So, for example, [] is already a postfix operator: it would be confusing if it was sometimes a postfix sometimes a prefix. And the unary * is already used to dereference, it would be weird if sometimes it was used to take a reference instead (for that there's already the unary &).
And don't forget the precedence: you shouldn't redefine it. So &int[] would mean &(int[]) not (&int)[].
And since we're talking of a hypothetical new language, for function types I'd choose to re-use an existing operator although for a completely different meaning: ->
Note that structs and typedefs that you define are proper type names, you can switch out "int" for them. You can easily "typedef int *intPtr;" and then merrily go "intPtr myptr".
(I think cast type-specs might be a bit of an exception here were the rule breaks a bit -- because they don't "use" a name in the cast "expression". I don't intimately understand how those work. The whole thing is an idea that was stretched to the limits, especially with C++ where consistency has pretty much been given up).
:does this look correct to you?
I can only assume he removed them, not for brevity, but as a compromise for the sake of C programmers to only ask them to learn one new thing rather than two.
https://c-faq.com/decl/spiral.anderson.html
But maybe also because I changed up my general programming approach a bit and try to think a bit longer before hitting up a search engine. Also forced myself to read more reference sheets than stackoverflow or chatgpt. And I must say, this made me a lot more comfortable, because I understand more. Can recommend to go through the struggle/10
The same trick works for anything you want to typedef!
Consider this type declaration in Go:
This took me 5 seconds to write. The C/C++ counterpart would probably take an hour to get right :(Oh, functions can't return arrays in C. That would be a pointer. Well, that makes it easier.
It did take a couple minutes, but I just woke up. Also, idiomatically there would be a typedef: That took a few seconds to write.What a pointer to function (int) returning int?
The difficult way:
The easy way: The typedef reads just like a normal function declaration except for the name is the type name rather than the function symbol name. Just like a function declaration, you can add more documentation by giving the parameters names. e.g.: Similar trick can be used for pointers to arrays (just typedef the array type). Although in practice pointers to arrays just make for awful code.Although if I was writing this myself, I would split it up like so:
Which is equivalent and much easier to read. (Although still not trivial. I have no idea what purpose this type really serves. The type names are placeholders as a result.)C type declarations become particularly tricky specifically when people try to directly declare (or typedef) pointers to functions/arrays without an intermediate typedef for the function/array type.
So if you make it a rule to typedef array types before creating pointers to them (although this usually makes for some rather unreadable code so maybe try avoiding it in general) and to typedef function types before creating pointers to them, you will usually have much more readable code.
As a bonus, you can then use the function typedef to provide an early type-check for a callback function (even if it's a callback your code doesn't directly use). E.g.:
Or, you know, switch to a higher level language.In B the only type is the machine word, which can perform arithmetic and be dereferenced. Arrays the only special kind of declaration and are declared like
Where auto is a storage class specifier like extern or static.In that world, having declaration follow usage makes perfect sense. There's no "other" kinds of data to worry about, the operators you can apply to an expression describes its type exactly.
https://www.digitalmars.com/articles/C-biggest-mistake.html
The "fix" would be to not have arrays decay. I believe C++ could probably get there if it had taken Epochs, I further believe taking Epochs (P1881) when it was proposed would have been extremely disruptive but perhaps possible and worth attempting. I do not believe it's still possible, the moment passed and with it, in my view, any hope of salvaging C++.
Without Epochs, any and every such change to C++ is similarly disruptive and that's too expensive so more or less nothing gets fixed, instead layers of kludges must be added. That's how Foonathan ends up with: class foo final trivially_relocatable namespace() { … };
As for the billion dollar mistake, a null pointer reference results in a seg fault. But the array decay results in buffer overflows, the #1 problem in shipped C code, and the buffer overflows result in memory corruption. Memory corruption is much, much, much worse than a seg fault. For example, buffer overflows are exploitable by malware. Seg faults are not.
Hence, the array decay is the worst mistake.
> semantics
Accessing a[i] means i gets checked against the array length, and it's a fatal error if it doesn't.
> ABI
Same as struct {size_t length; void* ptr;}
> size
sizeof(a)
> length
__length(a) or something like that
We invent new languages almost daily and none of them have ever resolved the problem. I mean, when you start with a pointer, it's uninitialized. Setting it to null is much better than having it point somewhere, where there might be incorrectly interpreted data.
So, null. We have Java, suffering the same problem, Golang suffering the same problem and Rust, where you have the same problem if you call unwrap, without checking that all code paths ensure you have the value populated.
Seems to me this is an inherent problem of using an uninitialized value... so, what is the mistake?
The mistake is that Nothing isn't a kind of thing. It's a categorical mistake, enshrined in many programming languages because it was easy and they didn't realise it was a bad idea, that's what Tony's talk is about - he's the one who made the mistake originally.
> I mean, when you start with a pointer, it's uninitialized
You're talking about a language implementation detail. Indeed I'd say plainly, a language defect, particularly notable in C (and so C++) where it continues to cause problems.
> We have Java, suffering the same problem
That's correct, all non-primitive Java objects can be null instead.
> Golang suffering the same problem
That's correct, Go chooses "make representable states legal" rather than "make illegal states unrepresentable" and so every type must have a legal "zero" state.
> and Rust, where you have the same problem if you call unwrap,
Nope. Try it. Call "unwrap" on a Vec, or a HashMap, or a File. You can't, there is no such method, it's a method on Option.
> Seems to me this is an inherent problem of using an uninitialized value... so, what is the mistake?
Goodness no. An actual uninitialized value by default would be a design mistake. They're currently trying to fix this mistake for C++ 26. If for performance reasons you need to delay initialization you need a trick like Rust's MaybeUninit<T> wrapper. You certainly can't paper over it with null.
Still an insane data structure, and I'm still learning Zig, so I can't promise this is precisely equivalent to the C, I got as close as I could and am unwilling to try and compile, let alone build, such a type.
No one would actually do this unless they needed `extern` compatibility. Ignoring the fact (pointed out in the Fine Article) that no one would use anything vaguely like this, a more Zig native version might be
Which returns a slice of slices of immutable u8s, these being fat pointers which include their length.I believe dcl mimics the cdecl program.
charへのポインタの配列へのポインタを返す関数へのポインタへのポインタの8要素からなる配列の未定サイズの配列
which uses the reverse of English possessive, and object verb order.
cdecl> declare bar as const pointer to array 5 of pointer to function (int) returning const pointer to char
char * const (( const bar)[5])(int )
Also online at https://cdecl.org/
Could we have done without pointer arithmetic? Could we have hidden the pointer part of function pointers and made them syntactically just like Python or JS functions?
Did arrays as raw blocks of memory ever need to exist?
I remember when I first learned C, I didn't really see the problem, I thought it was a perfectly fine language, until I learned Python/JS/C++/Dart and saw how nice high level features and package management is.
But now I wonder how I ever used it at all without complaining!