int[4] arr; // oops, doesn’t compile
Or a function pointer: int (*)(int) fptr; // oops, doesn’t compile
So much for the simplicity of C declaration syntax. int[4] arr; // oops, doesn’t compile
Or a function pointer: int (*)(int) fptr; // oops, doesn’t compile
So much for the simplicity of C declaration syntax.The correct way is to write
int arr[4];
and to interpret it as "arr[4] is an int" (which is only a slight lie because arr[4] is undefined if arr is a 4-element array).How do you declare an array of pointers? Again, you write
int *arr[4]; // array of 4 pointers to ints
because in C expression syntax, "* arr[4]" means to index into the array first and then to dereference. If that is an int, it means that arr[4] is a pointer to an int, and consequently arr is an array of pointers to ints.If you want a pointer to an array of ints instead, do this
int (*arr)[4]; // pointer to an array of 4 ints
again, because that's how regular C expressions work. Functions are (mostly) not an exception: int myfunc(int x);
int (*myptr)(int x);
which is to say that "myfunc(x) is an int" and "(* myptr)(x) is an int", i.e. myptr is a pointer to a function that takes an int and returns an int.Note that in the beginning (i.e. K&R C, pre-1989) the way to declare functions was consistent: Declarations had to be
int myfunc(x);
i.e. there was no types in the argument lists. The types in the argument list appeared, I believe, after Stroustroup added them to C++ in order to improve type-safety.It also seems telling that no recent language has followed C’s example for declaration style, which is more implicit than explicit.
> It also seems telling that no recent language has followed C’s example for declaration style, which is more implicit than explicit.
Actually most languages don't let the user do what C declarations let you do. For example, in Java (almost) everything is an object, and you can't just create a triple-indirected pointer. So, these languages can afford a declaration syntax that is less potent.
And then there are other more systems-oriented languages that chose to not copy C declarations. They come with their own gotchas. As examples I will pick D and Rust.
In D, you create a multi-dimension array like this: int[5][10] arr; Leading you to believe that you can use it as arr[4][9]; Wrong. That's an out-of-bounds error. You need to write arr[9][4]. Now, was that totally not confusing? The alternative is to expand these types systematically to the left, i.e. write [10][5]int, and maybe move the type to the right of the variable name, as in "let arr [10][5]int;". Honestly I don't like that either.
I've never really used Rust (either), but its downside, in my opinion, is that it has much more distracting syntax / punctuation.
I would love if there was a uniformly better way to declare things than the C way, but I still think C has the best tradeoffs for my practical work. The next time that I toy with language design I might try to simply go with C declarations, prefixed with a sigil or "let" or something, to remove the need for the lexer hack.
Not really. You can't just use _an_ expression, you have to use a specific expression. For example, * ppX is a perfect valid expression for a pointer to a pointer named ppX, as in:
int **ppX;
if (*ppX == NULL)
So you need to use an expression where the declared variable is used that results in a non-pointer type. And then, there's actually more to it... only certain types of expressions are valid. E.g. this isn't a valid declaration for a pointer, even though it's valid as an expression: int pX->;
I find that basically anytime somebody tells me that C rules are to "simply [...]", they've inevitably ignored a whole bunch of cases. Your post is no exception.I think you are wrong here, you can't even make something other than what "results in a non-pointer type". Because by definition, you're making the type to the left with the expression. (That could still be a pointer type if it is a typedef'ed type; such as "typedef int * intptr; intptr x;" but I don't think you meant that by "pointer type" [0]).
And your first example is perfectly syntactically valid (other than missing the conditional statement that must follow the if-condition).
And no, "pX->" is not a valid expression. Was that a typo?
And yes, only a subset of expressions are valid. Basically, the expressions that you can form by applying subscripts, (x[3]), dereferences (* x), and function calls. Because, a declaration like "int x + 3;" or even "int x + y;" just doesn't make sense. I don't see a problem there.
Btw. I'm not saying that C as by the current standards is super straightforward and pure. It's definitely not, and C does actually have a lot of historical baggage that makes our lives a little harder. I'm just explaining the underlying unifying principle, which IMHO is actually nice. And honestly it seems you, too, are still confused because there is just a lack of clear explanations about C declarations. That principle should be much more well-known, and almost all problems that novices have with declarations are unnecessary frustration that they wouldn't have if someone would have told them the trick.
[0] By the way, typedef is another thing that seems to be super obscure, while it is extremely simple: It's just a keyword that modifies declarations to declare an alias for that type, instead of a (named) variable of that type.
No. The "interpretation" is "arr is an array of ints", and that is its type.
The complexity of this declaration is evidenced by the fact that you spend another page of text "randomly" adding characters and delimiters around variable declarations to change its type:
int arr[4] // array of ints
int *arr[4] // array of pointers to int
int (*arr)[4] // pointer to array of ints
These are all changes to type. Yet, instead of changing the type declaration, a bunch of stuff is added all around the variable. And you have to come up with ridiculous explanations like "arr[4] is an int which makes arr an array of ints".That's exactly why most languages said: "if we're changing the type, we're going to reflect this in the type". In a better world the examples above would be something like
int[4] arr; // array of ints
*int[4] arr; // array of pointers to int
*(int[]) arr; // pointer to an array of intsI would have been happier if I had found my explanation interpreted in a more generous way. But that's basically what I was saying (and literally what I was saying in another comment).
As to the rest, the advantage of the C approach to type declarations is that there is no type declaration syntax. Just expression syntax. And that it's very terse.
> That's exactly why most languages said: "if we're changing the type, we're going to reflect this in the type". In a better world the examples above would be something like
There's a problem in that your proposed syntax is not even properly parseable. How would a parser recognize that your lines start with types i.e. are variable declarations? For example the example "* (int[]) arr", it would start reading the asterisk and the opening parenthese as an expression, and then suddenly find a type name (int), and could then not throw an error if it was one, but had to start all over again and try to parse the whole thing as a type declaration. That's not exactly nice - good syntax is parseable with a single token of lookahead. That not only makes parser implementations easier, but is also easier to read for humans and leads to better error detection.
Apart from that I think that your examples are about what D does, and this stuff is WORSE in my opinion. While the real problem with C declarations, which is the need to thread a symbol table through the lexer/parser, is still existent in D syntax (I believe), it introduces other problems:
How do you use an array that was declared as "int[5][10] arr"? Using it as "arr[4][9]" is an error: it must be "arr[9][4]". In other words, your approach to type declarations requires the programmer to constantly turn around declarations in his/her mind, which leads to lots of mistakes. It gets even harder when you add pointers / functions, for example "int* [5][10] arr" I believe you must access as "* arr[9][4]", or whatever the D dereference syntax is.
Java can afford to let you declare "int[][] arr = new int[5][10]" and let you access "arr[4][9]", at the cost of cheating. Java can "turn around" the dimensions because it doesn't actually have an "algebraic" type syntax, which it doesn't need because it doesn't have pointers / function pointers so there is no interaction there.
That's one of the reasons why most newer languages have the type to the right of the variable name, and types grow to the left (towards the variable name). For example, "let arr: [5][10]int" you can access as "arr[5][10]" which is easier, but that principled approach to syntactic construction of types also puts requirements on the expression syntax: For example, "let arr: [5][10]* int" would have to be accessed as "* arr[5][10]", which is weird - or the expression syntax must be changed to use a postfix dereference operator.
In short, it's not as easy as you thought, and the C syntax is in fact pretty smart. And from a practical standpoint I prefer the C way very much because it's so much terser and has less punctuation than all the alternatives. The only thing that annoys me is the lexer hack.