9 ms·
C "clockwise/spiral" rule to understand declarations
- pwdisswordfishz 2y agoEh, this misconception again.
- userbinator 2y agoOver 30 years and it still won't die. Sadly the link to Linus Torvalds' explanation of why it doesn't work has died, but the Archive remembers: https://web.archive.org/web/20141218085356/https://plus.google.com/+gregkroahhartman/posts/1ZhdNwbjcYF https://web.archive.org/web/20141218085356/https://plus.goog...
- ykonstant 2y agoIn that thread, replying to a suggestion to use typedefs, Kroah-Hartman states: > Heh, no, typedefs don't help anything, there's a reason we don't use them in the Linux kernel whenever possible. This surprises me, can someone explain that remark and elaborate on the reason it alludes to?
- gregkh 2y agoThey hide structures very easily, allowing programmers to accidentally put them on the stack or use them as parameters in functions where they shouldn't be doing so. By forcing "struct" on the name, it makes it more obvious as to what you are doing.
- pwdisswordfishz 2y agoThat's not a reason not to use typedefs at all.
- casenmgreen 2y agoTypedefs are useful only if you use functions for all operations on the data type. Otherwise, if you actually use the variable directly, you are requiring the user to remember the underlying type of the typedef. Larger program, hundred plus typedefs, unbearable burden on reader.
- wruza 2y agoThe proper way is to use /usr/bin/cdecl or https://cdecl.org https://cdecl.org and extract as many typedefs as reasonable from the gibberish, because in C most of the times you’ll need these anyway to address lifetimes and ownership/borrowing points.
- palotasb 2y agoThe spiral rule works only if there is no pointer to pointer or array of array in the type. In other words it is an incorrect rule. But take this for example: +----------------------------+ | +-----------------------+ | | | +------------------+ | | | | | +-------------+ | | | | | | | +--------+ | | | | | | | | | +--+ | | | | | | | | | | ^ | | | | | | int * * ¦ ¦ ¦ VAR[1][2][3] | | | ^ | | | | | | | | | | | | | | | | +-----+ | | | | | | | | | +----------+ | | | | | | | +---------------+ | | | | | ---------------------+ | | | +-------------------------+ | +-------------------------------+ The type of VAR is a [1-element] array of [2-element] array of [3-element] array of pointer to pointer to ints. I drew a spiral that passes through each specifier in the correct order. To make the spiral correct it has to skip the pointer specifiers in the first three loops. This is marked by ¦. The Right-Left Rule is quoted less frequently on HN but it's a correct algorithm for deciphering C types: http://cseweb.ucsd.edu/~ricko/rt_lt.rule.html http://cseweb.ucsd.edu/~ricko/rt_lt.rule.html The spiral rule can be modified to process all array specifiers before all pointer specifiers, but then you'd have to specify that the order to do so is right and then left. At that point it's just the Right-Left Rule.
- rcxdude 2y agoI've found that 'declaration follows use' is actually the easiest way to understand C type declarations. The only annoying to remember thing is which way the various type specifiers like const bind.
- pwdisswordfishz 2y agoIf you're going to adjust willy-nilly on which loop of the spiral a given element appears, then it's not much of a rule, isn't it? I mean, why not write this? +----------------------------+ | +-----------------------+ | | | +------------------+ | | | | | +-------------+ | | | | | | | +--------+ | | | | | | | | | +--+ | | | | | | | | | | ^ | | | | | | int ¦ ¦ * * ¦ VAR ¦ [1] ¦ [2][3] ¦ ^ | | | | | | | | | | | | | | | | +-----+ | | | | | | | | | +----------+ | | | | | | | +---------------+ | | | | | ---------------------+ | | | +-------------------------+ | +-------------------------------+
- fc417fc802 2y agoIt's unfortunate that we're stuck with syntax that so many people struggle to accurately decipher.
- userbinator 2y agoIt's unfortunate that so many people never read K&R. A very small yet readable book, written by the original authors of C, that very clearly covers the language and even presents a C program to parse the declarations.
- xigoi 2y agoIf you need a program to help you read C programs, that suggests a serious flaw in C.
- f1shy 2y agoYou do not need it. But is a clear and handy way to present the logic/algorithm, if you are learning C anyway. I would even say, that if C allows for a clear description of the procedure, clearer as plain text, it indicates how good and expressive C is…
- gf000 2y agoThere are books on hieroglyphs as well, but there is a good reason we don't use them as types in programming languages.
- masklinn 2y agoYou’re only “stuck” with it to the extent that you’re stuck with C(++). Free yourself from that and the rest follows.
- immibis 2y agoC++ is better, actually, because a template type has a straightforward syntax.
- HexDecOctBin 2y agoI just spent a week trying to write a parser for C declarations. Recursive Descent didn't work, Top Down Operator Precedence didn't work, I finally found an simple implementation of cdecl in K&R2, but it printed out textual description instead of creating an AST. Converting it to create an AST was a whole different challenge. In the end, I got it working by adding the idea of a blank-type and doing some tree inversions (almost using each AST as a program that executes over the nested chunk of AST). It was only about 200 lines of code, and yet never have I been happier to finish a solution and never having to think about it again.
- userbinator 2y agoI finally found an simple implementation of cdecl in K&R2 Perhaps you should've started learning C with K&R. ;-)
- HexDecOctBin 2y agoI did, 10 years ago. Didn't remember every piece of code.
- not-my-account 2y agoWhat are you working on?
- HexDecOctBin 2y agoA C parser for a meta-programming layer (automatically generating serialisation routines, etc.)
- Galanwe 2y agoJust my 2 cents: The _external_ way of doing introspection (a parser like yours) is generally very limiting in real life, as you will have to interpret all the preprocessor directives that your code follows. e.g.: struct Foo { #ifdef ... int a; #else float b; #endif } Not only will you have to feed your parser the exact same inputs as your build system, but also some directives are built-in the compiler and may be hard to replicate. The easiest way to do introspection is _intrusive_, even though it pollute the code a bit. e.g. DECL_STRUCT(Foo) { DECL_ATTR(int, a); } END_DECL_STRUCT A long time ago, Qt had a sort of hybrid intrusive parser like this (MOC?), that was 20y ago though things may have changed.
- LysPJ 2y agoAs a C programmer, declarations in Go appeared "backwards" to me when I first saw them. IMO the Go syntax is a vast improvement as it's much simpler and avoids the clockwise/spiral issue: https://appliedgo.com/blog/go-declaration-syntax https://appliedgo.com/blog/go-declaration-syntax
- unrealhoang 2y agohence why every recent programming languages are type after name (go included).
- gf000 2y agoI think a proper type system that doesn't special-cases arrays are better, e.g. Array<String>. Pointers may also be part of this uniform structure, e.g. Array<Ref<String>>, and there is zero question on how to parse it even if you are not familiar.
- unrealhoang 2y agothat's the beauty of type after name, it's all left to right, even in special-cases of array, references, and the most confusing-contributor to cdecl: functions `func(int)[5]*string`: perfectly clear, just read left to right.
- gf000 2y agoWhat about a Map<int, String>? You can't really encode a tree structure with only linear semantics (without at least something like s-expressions).
- unrealhoang 2y agoAnd type declarations are s-exprs. The advantage of type after name is that it keeps the traversing order consistently pre-order (node, left, right), instead of either: - the notoriously ridiculous spiral cdecl: reading stuff right (output), node (func name) and left (input) - create a new name to describe the function in pre-order: Func<Input, Output>
- the_gipsy 2y agoIn example #2 it skips arbitrary "tokens": the first right parenthesis is visited, but its matching left parenthesis is skipped.
- f1shy 2y agoI find this description way better: http://unixwiz.net/techtips/reading-cdecl.html http://unixwiz.net/techtips/reading-cdecl.html
- keyle 2y ago(1994) but oh, still relevant.
- casenmgreen 2y agoFor non-array types, I write mine reversed, and read them right-to-left. int long long unsigned number_of_days; - read it right to left, an unsigned long long int float fraction; - read it right to left, "" reads as "pointer to", so pointer to float
- casenmgreen 2y agoCurious. My "float *" was somehow changed to "float fraction". Ah, the asterix became italics.
- foldr 2y agoFor future reference, you can produce a literal * by putting a \ before it.
- dapperdrake 2y agoUnixWiz put it well: "Go right if you can. Go left if you must." [1] [1]http://unixwiz.net/techtips/reading-cdecl.html http://unixwiz.net/techtips/reading-cdecl.html
- immibis 2y agoThe actual rule is "declaration follows use". int p[5] means the type of p[5] is int (but you still have to remember valid elements are 0-4). void (signal(void()(int))(int) means the type of (signal(something that is a void()(int))(42) is void. And void(p)(int) means the type of (p)(42) is void. If you can remember the precedence of these operators, you automatically remember the precedence of their "type operators" as well.
- nialv7 2y agoyeah, this is so simple, i don't understand why people keep invent more complex and worse rules to understand cdecl...
- alkonaut 2y agoI never understood why the asterisk is with the variable name in C and not by the type. Apart from declaring multiple variables of pointer and non-pointer type at the same time is there any other reason for it?
- dataflow 2y agoIt's because they chose the declaration syntax to match the usage syntax, rather than as a modification of the type. i.e. long (*f)(int); means, if you dereference f (i.e. write *f), you get long(int). If you then call it with an int, you get long. It's pattern matching against the usage.
- deleted 2y ago[deleted]
- tpoacher 2y agoIt's not (unless you mean "by popular convention", which actually is not necessarily that popular). int* c is perfectly valid syntax and so is int * c. The latter is my preferred convention: it helps me think of the asterisk as an operator, which, when acting at the "declaration" level, is binary in nature. Whereas when the asterisk is used at the 'expression' level, it is unary in nature, and can be thought of effectively as a completely different operator that happens to share the same name/symbol as the declaration-based one.
- tengwar2 2y agoI'd advise not doing "int* c" as it is misleading. Consider int* a, b; This looks as though it is equivalent to int* a; int* b; but actually means int* a; int b; Most people trying it your way hit this issue fairly quickly and revert to "int *a". It's not just a meaningless convention; it's a convention which reflects the grammar of the language just as conventions on indentation reflect it. "int * a" is not as bad, in that it is ambiguous rather than misleading, but I would avoid it for similar reasons.*
- tpoacher 2y ago
- weinzierl 2y agoAre there any tricks to make Rust type declarations easier?
- unrealhoang 2y agoyes, read it from left to right, don't use spiral.
- weinzierl 2y agoSure, no spiral, but I still find the associativity of modifiers confusing. For example could you tell the difference between & mut &, & & mut and & mut & mut right off the top of your head?
- unrealhoang 2y agoyou should just see &mut and & as 2 separate modifier, maybe that'd help. - & &mut doesn't make sense: and a variable of type & &mut can't mutate the underlying object, it's practically equivalent to a & &, even worse, since the inner is &mut, you can't have 2 & &mut that point to the same &mut (mut xor shared). - &mut &: mutable reference that point to a shared reference, means you can change the outer to point to another shared reference, but you can't change content of the inner, for example: let a: &str = "hello"; let b: &str = "world"; let mut c = &a; let d = &mut c; *d = &b; // (*d).make_ascii_lowercase(); // not allowed - &mut &mut: similar to the above, but you can also change the content of the inner.
- sylware 2y agoPeople should stick to the specifications... That said, that makes me think of perl5. I though the perl5 coders were doing some kind of sick competition: who is going to use the most implicit code, namely to read the code you would need a complete/perfect/permanent understanding of the full perl5 syntax parser to understand what some code is actually doing. I hate perl5 for that, but ultra complex syntax computer language like c++ and similar (java, rust, etc), are worse. In advanced OOP, you have no idea of what's going on if you don't embrace the full OOP model of a complex program, not to mention it does exponentially increase the syntax complexity, which is a liability in order to get a sane spectrum of alternative "real-life" compilers since those become insane to implement correctly. If implicit there is in a computer language, it must be very little and very simple, but should be avoided as much as possible. Does it mean more verbose code, well, most of the time yes, and this is usually much better on the long run. For instance in C, I try to avoid complex expression (often I fail, because I am too used to some operators like '++' '--'), many operators should not be around, not pertinent enough (like a ? b : c) only increasing compiler complexity.
- foldr 2y agoAfter implementing (parts of) a recursive descent parser for C, I had a sudden enlightenment about how easy it is to understand C declarations once you get the basic principle. There's already a comment on this (https://news.ycombinator.com/item?id=42565459 https://news.ycombinator.com/item?id=42565459), but I'll try to go into a little more detail. A lot of people start with the idea that the syntax of C declarations is '<type> <variable_name>'. This works fine for simple cases, but it's completely wrong. What a declaration like 'int x' actually means is the following: declare a variable x of a type such that the expression x is of type int In such a simple case this seems unnecessarily long-winded, but now let's look at a more complex case: int (*p[4]) (int x, int y); declare a variable p of a type such that (*p[i])(x,y) is of type int If dereferencing the ith element of p and then calling the result with two arguments gives us an int, then p must be an array of pointers to functions that take two arguments and return int. If you saw the expression '(*p[i])(x,y)' in some code, you'd have no difficulty figuring out that p must be an array of function pointers. So you needn't have any difficulty when reading the declaration either. One slightly confusing thing here is that the nesting of the expression syntax is the opposite of the nesting of the type. The expression is funcall(deref(array_index(p, i)), [x, y]) whereas the type is array_of(pointer_to(function(args: [int, int], returning: int))) This makes sense once you understand that the expression is to be interpreted just as a normal expression. The first thing you do with an array of something is index into it. So indexation is going to be the most deeply nested part of the expression, even though the outermost layer of the type is 'array of ___'. One additional source of confusion is the '*' operator and the need for additional parentheses in function pointer declarations. In C, function pointers dereference to themselves, so 'p()' and '(*p)()' are equivalent if p is a function pointer. However, in a type declaration you need something to distinguish a function pointer from a function, so the '*' has to be present. Why can't we just write 'int *p[4](int x, int y)' in the example above? Because of how the operator precedence rules work. That expression is equivalent to '*(p[4](x,y))', so it would declare an array of functions returning pointers to integers. (You can't declare an array of functions in C, so that's invalid.) Ad-hoc rules for interpreting C declarations miss the genius of their underlying concept. You already know all the syntax you need to understand a C type declaration! It's just C expression syntax.
- 2y ago
- Ferret7446 2y agoI've found it much easier to learn that C declarations are based on how they are used, than try to remember this (IMO very unhelpful) "spiral" rule. If you have int on the left and some kind of declaration for foo on the right, that means that when you use foo with all of the present syntax, you get an int. For example, in char *(*fp)( int, float *) if you use fp like this *(*fp)( int, float *) you get a char.