8 ms·
So You Think You Know C? (2020) [pdf]
- rwmj 3y agoI know he's trying (badly) to make some point about undefined behaviour and/or strict reading of the C standard, but his answers are simply wrong. The first case, for example, returns 8 [edit: the size of the structure] (not "I don't know") under any reasonable compiler, and isn't even UB. The second one returns 0, and is also not UB. etc. Also if someone wrote this bit of obscurantism in any programming language it should be rejected hard when reviewing the code: return (((((i >= i) << i) >> i) <= i));
- amalcon 3y agoThat's an illustrative example. You're not meant to actually use such code, but the relevant insight (that code with binary shifts is rarely portable with types of unspecified size) is accurate.
- nuc1e0n 3y agoI understand where you're coming from, but have you tried them? I think there's a good chance the first question will return 16 on 64 bit systems.
- tyingq 3y agoReturns 3 on Borland Turbo C, which I think most people would say is a reasonable compiler.
- matja 3y agoDepends on the ABI which the compiler targets. The vast majority of 64-bit OS's are ILP32, LLP64 or LP64, where ints are 32-bit - but to the paper's point, you don't know until you compile it.
- dzaima 3y ago"are simply wrong" and "under any reasonable compiler" don't mix - yes, for arm(32/64)/x86(-32 or -64) there are common values, but that doesn't make everything else not exist. OP doesn't say that the first two are UB, just that you don't know what they will be without additional information. For example, here's a GCC-supported target that's available on Compiler Explorer, where the result of question 1 is "3", and question 2 is "1": https://godbolt.org/z/MjT8YhrTc https://godbolt.org/z/MjT8YhrTc
- rwmj 3y agoSure, that's an AVR platform where structs are packed and ints are 16 bits, and it returns the size of the struct, in other words, the correct answer. And that's my point, it returns the size of the struct, it's not "don't know", it's not doing anything weird, so why have the question?
- dzaima 3y agoBut, thus, upon seeing the program, without additional information you cannot know whether it'll give 8 or 3 (or who knows what else), and thus "I don't know." is very much an appropriate answer. Of course, it might be more clear to say "it depends" or "there isn't enough information to answer", but I'd say "don't know" is nevertheless a correct answer. Definitely more correct than "8" at least. "it's not doing anything weird" only to people who do already know the C standard inside out. But there is a significant amount of people who might not know everything C (or might know that there are some weird things about types, but still assume 'int' will be at least 4 bytes or something). It's of course not a question of much practical impact (for anyone not working in embedded at least) but it's nevertheless one that can be at least interesting to some.
- rwmj 3y agoThe only time you're writing sizeof(x) is when you want to know the size of 'x', eg. to store it somewhere else or zero out the memory or something of that sort. And it gives the right answer, great! It doesn't ever do something that's undefined or strange, and it's not an obscure part of the C language.
- richardjam73 3y agoI got the first example to give 4 and the second one returns 1. OpenWacom DOS 16Bit. Under 32bit it gives 8 AND 0.
- mannykannot 3y agoMany errors have arisen from someone tacitly assuming what "any reasonable compiler" would do. These questions barely scratch the surface of the possibilities. Furthermore, just because your code works with Reasonable Compiler 1, it does not mean it will work with Reasonable Compiler 2. I think I have seen enough C compilers that, for each of these questions, you could pick a pair giving different answers.
- AnimalMuppet 3y agoWorse, Reasonable Compiler 1 may give different results, depending on what flags you pass it at compile time.
- krychu 3y agoYour code dictates the compiler's behavior. If you grant the compiler flexibility, it's unreasonable to complain about unexpected results.
- mannykannot 3y agoI don't think it is a complaint; it's an observation in support of a point of view.
- nanolith 3y agoIt's fine to bristle about underhanded evangelism (e.g. "Use Rust instead of C, because C is a foot cannon and Rust is perfect"), but that's definitely not the case here. This is a reasonable critique of the common C pitfalls. I use C daily. The answer in each category is "I don't know" because, without the additional information provided by a specific ABI, specific target, and specific compiler, these are all undefined. No language is perfect. It's important to understand the limitations of a language to use it effectively. That is closer to the author's point. This is not a condemnation of C, but rather, an acknowledgement of C, warts and all. The original point of C was to provide a portable low-level language that could capture expressions instead of writing everything in assembler, so that UNIX could be ported to platforms other than the PDP-11. It's also a reason why C still has uses not shared by newer languages, because these newer languages are more precise. This is a Good Thing, but unfortunately, developers who are unaware of this imprecision can make mistakes that aren't portable and may not behave as they assume. These problems appear, seemingly out of nowhere, when porting source code to a new compiler or a new platform. While the examples the author provides are over-the-top and easy for an experienced C developer to catch, there are more subtle examples, such as promotion and signed integer overflow, that are hard even for an experienced C developer to catch. This is why I use static analysis, model checking, proof assistants, and even a modern compiler with built-in pedantic checking against the standard. This language has subtle corner cases that even someone with 25-30 years of experience can miss. Unit testing can miss these corner cases unless the right input values are chosen, and the right input values are not always so easy to guess. It's not a condemnation of C or an attempt to evangelize newer languages to point this out, but rather, an acknowledgement that mastery of any language means accepting both its strengths and limitations. C has both.
- cuanim 3y agoHello, what would be your recommendations for someone who wants to take the next step from basics of C to contributing to FOSS repos (for ex: FFMPEG or VLC or mpv)? I have been working my way through [Modern C](https://inria.hal.science/hal-02383654/file/ModernC.pdf https://inria.hal.science/hal-02383654/file/ModernC.pdf) but I can't seem to figure out how does one start with making(/contributing to) non trivial projects.
- virtuous_sloth 3y agoYou can't say that with certainty without specifying the compiler, it's version, and the architecture.
- P-Nuts 3y agoI have debugged a problem that was due to a left shift of 1ULL by 64 (in a routine that tried to set the lowest n bits by shifting 1 left and subtracting by 1). I needed to read the Intel docs to find out what actually happens to work out how badly we’d corrupted the customer data and how to write a fix to unmangle it. What Intel processors do is ignore the upper bits of the shift quantity, so it’s effectively (1ULL << (x % 64)) - 1, therefore the function that should have set all ones for x=64 actually set all zeros.
- nuc1e0n 3y agoVery cool. I think this says quite something about the modern human thought process in general. We place a lot of emphasis on knowing how abstract rules we've created should interact and not so much on actually testing things for ourselves. Sometimes there are unknown factors that will influence results, such as undefined behaviour with C compilers in this case.
- Buttons840 3y agoWow, the HN front page at any given moment gives fewer things to talk about than this one PDF. This is 10 blog posts in one PDF, and each post is good.
- notquiteright 3y agoI knew these were all ”I don’t know” edit: but yes, deceptive phrasing by the quiz author. If you don’t know that, then yeah, you’re probably not a great C programmer. Adjust your mental model: C is meant to be useful on a very wide variety of platforms, including ones that don’t follow the conventions (ASCII character table, size of byte, alignment requirements, etc.) of the most common platforms. Even including knowledge of what is IB and UB, C is still simpler than most languages in common use these days.
- BruceEel 3y agoRight. Personally, I think those could have been better phrased as "I can't tell". As for C's simplicity, 100% what you said.
- movedx 3y agoWould you write a web app in it? (Just out of interest. I'm not trying to be an ass.)
- notquiteright 3y agoNo, wrong level of abstraction. Why do manual memory management in a realm where it doesn’t really matter? In addition to the excessive amount of work (also why I wouldn’t choose Rust for the job), you’re opening yourself up to a whole class of errors for no good reason.
- addaon 3y agoI've written plenty of C web apps back in the old cgi-bin days, for no particular reason, and haven't found the memory management an issue. cgi-bin is a great example of a lifetime-managed execution environment where you can just use the heap as one big arena allocator, with malloc() having unchanged semantics (or, if you're feeling fancy, going to a bump allocator), and free() being no-op'd (or, more realistically, just omitted when writing code). Yeah, your high water mark memory usage might be a bit higher than a more managed approach, but the OS is a perfect garbage collector, and the fastest possible garbage collector, for short-lived executables. The bigger issue is the mediocre string processing libraries that are so common to C.
- lofties 3y agoNo.
- unwind 3y agoIt irks me no end when language-lawyering like this uses sizeof in the stupidly obscure way of the first example. sizeof(*(&s)) I mean, wut? Really?? This should just be sizeof s Since sizeof is not a function, and redirecting by dereferencing a pointer that is just created by taking the address of a local is silly. Not overly impressed by this.
- flyingcircus3 3y agosizeof still requires parentheses if it's a type and not a variable. To me it's like curly braces' optionality on conditional blocks with one line. If you aren't consistent, you're setting yourself up for heartache.
- jstimpfle 3y agoI think this is the same syntax as the one used for casting -- a "type expression" will only be recognized immediately after an opening paren (or at the start of a statement, in case of a declaration statement). So this is rather consistent.
- flyingcircus3 3y agoConsistency would be using parentheses for all cases of sizeof, because people without 10 years of experience can pick it up.
- jstimpfle 3y agoIf you think something is inconsistent you should ask why and find that what you thought inconsistent are simply 2 different things. The parentheses have nothing to do with sizeof, it's just how you write types (I'm repeating myself).
- mrkeen 3y agoHere's Torvalds on it: > But "sizeof()" really is a function. It acts exactly like a function of it's argument. There is no reason to not treat it that way. https://lkml.org/lkml/2012/7/11/103 https://lkml.org/lkml/2012/7/11/103
- dahfizz 3y agoKinda disappointed that all five questions just boiled down to the size of structs / ints in memory. There's way more interesting and arcane gotchas in C!
- mtreis86 3y agoCan you elaborate a bit? I don't know enough C to know many gotchas besides the order of the characters in i++ vs ++i mattering.
- esrauch 3y agoEvery other one can be justified by the size of char/int are not fixed, but instead the standard defines a minimum size. Int is usually 32 bit but it's allowed to be 16 bit or anything larger. Char is usually 8 bit but can arbitrarily large if the platform "byte" is not 8 bits.
- skitter 3y agoSomething as simple as adding integers is tricky due to its implicitness. Due to promotion, the following code works on some platforms but is verboten on others: int16_t a = 20000; int16_t b = a + a; But even ignoring size, it can bite you: unsigned int c = 1; signed int d = -2; if(c + d > 0) puts("1 + -2 > 0"); That code prints because of balancing: The signed value gets cast to an unsized int, which can't hold the value. For not directly integer related semantics, creating an out-of-bounds pointer is UB, unless it points to one past the end of the object, in which case you can create the pointer but can't read from it. And if an object's lifetime has ended, pointers to it aren't valid anymore even if you don't even read from it: void *foo_1 = malloc(1); free(foo_1); void *foo_2 = malloc(1); printf("Checking for same allocatoin: %d", foo_1 == foo_2); If you go by the standard, strictly speaking this is UB: #include <stdio.h> int main(void) { puts("Hello World); } But don't worry, real compilers will refuse to compile this. Most don't follow the standard anyways, because they don't treat writing to a memory location as side effect. A large source of gotchas in C are the standard libraries. For example here's a taste of the trouble its locales cause: https://github.com/mpv-player/mpv/commit/1e70e82baa9193f6f027338b0fab0f5078971fbe https://github.com/mpv-player/mpv/commit/1e70e82baa9193f6f02...
- sylware 3y agoC is the less worse alternative, the less worse compromise. C syntax is already way too complex. My opinion: a real world wide standard and royalty-free average modern ISA (for instance 64bits RISC-V), then significant system components get a native assembly version, and then some very high level interpreters (javascript?python?ruby?lua?tintin?etc?) written themselves in assembly. If the pre-processor is not abused (aka they are not trying to code c++ with the assembler pre-processor), we should reach a state much less toxic than what we have today.
- minroot 3y agoIt is our fault that we don't comprehend the genius of great men
- layer8 3y ago> How satisfied are you with the current state of software on a scale from 1 to -128? This is brilliant. :D
- irundebian 3y agoRegarding to the struct packing: That's why I love Ada, there are so called representation clauses which can define the size of struct-alike types called records. In C you have to rely on compiler options or features like __attribute__ which are not compliant to the ISO C standard. It looks like there is a ISO-C compliant version for that since C11 but it just doesn't look nice and like a first class citizen of the language. https://docs.adacore.com/gnat_rm-docs/html/gnat_rm/gnat_rm/representation_clauses_and_pragmas.html https://docs.adacore.com/gnat_rm-docs/html/gnat_rm/gnat_rm/r...
- svilen_dobrev 3y ago"So if I were to reinvent a programming language for the 21st century, I would reinvent being responsible instead. I would reinvent learning your tools; I would reinvent being attentive to essential details and being merciless to accidental complexity. Unlike the languages that come and go with fashion, the things that matter do deserve constant reinvention." (last paragraph of the book) --- can't upvote/ engrave it enough. Amen.
- okaleniuk 3y agoWow! Didn't expect to see it on Hacker News again. Hello, hi! I'm Oleksandr Kaleniuk, and I wrote the thing back in 2020. Please don't take it all too seriously. There was COVID-19, I had nothing better to do. I did write a new book since then, and my publisher would bite my head off for missing this opportunity. It's called Geometry for Programmers and it was published by Manning just this year. https://www.manning.com/books/geometry-for-programmers https://www.manning.com/books/geometry-for-programmers It's about curves, surfaces, transformations, homogeneous coordinates, rational splines, distance fields, and voxels. You don't have to know any advanced math to enjoy the book, but you might want to know a bit of Python. It's for programmers, so the didactic tools there are mostly code, not theorems.
- cerved 3y agoVery nice read, thank you for sharing.
- FpUser 3y agoI do not "know (tm)" any language but I could and do use many.
- Borg3 3y agoQ1:C Q2:A Q3:C Q4:D Q5:A And when I saw answers I smiled, Right! because he didnt specified platform. Yeah C can be devious, but I still love it! Its my favorite language. And Ruby for scripting.
- P-Nuts 3y agoI knew they were all UB except the integer promotion one. Ah well, at least I have -Wconversion turned on to save me from myself on that one.
- _gabe_ 3y ago> Eventually I learned I had to rely on the standard instead of folklore Isn’t this the opposite of what should have been done in the case of coding systems for a nuclear facility? I thought the whole point of the exercises was to show that C has lots of undefined behavior, so you should only rely on the behavior of the specific system you’re coding for. If the C standard doesn’t specify what happens when a signed integer overflows, that doesn’t mean you can just ignore it. You could crash the system, report an error, or find out what the targeted system is specified to do in the case of a signed integer overflow. Is there something else I’m missing here, or do the nuclear systems typically have several processors that may have varying behavior for this type of stuff?
- dannymi 3y ago>I thought the whole point of the exercises was to show that C has lots of undefined behavior, so you should only rely on the behavior of the specific system you’re coding for. In my opinion, the whole point of the exercise was to show that as an engineer, you should have an engineering mindset. And that means collecting requirements, selecting parts, reading a lot of specifications, and participate in writing them. C is an abstraction. If you program in C, you use that abstraction (so you use the standardized C abstract machine). Why would you program in C and NOT use that abstraction? If you want the latter, just use assembly. Especially in this application. Otherwise, yes, you hold yourself to the C standard, and you use compilers that are certified to actually implement that standard. Of course, there needs to be a lot (>50% of the total time) of testing, too. But not to figure out what the compiler does :P > If the C standard doesn’t specify what happens when a signed integer overflows, that doesn’t mean you can just ignore it. It means you have to write your programs in a way that signed overflow cannot happen. > You could crash the system, report an error, or find out what the targeted system is specified to do in the case of a signed integer overflow. Just make it fail (looong) before the signed integer overflow would happen (with proof). >what the targeted system is specified to do in the case of a signed integer overflow What does this mean? What is the "system" in this case? If the "system" is the hardware, then no. In standardized C, this overflow never makes it down to the hardware. If it's the hardware and the compiler, then sure, but you aren't programming standard C then. I guess you could do that (why would you, though... compliance is gonna reject it). > Is there something else I’m missing here, or do the nuclear systems typically have several processors that may have varying behavior for this type of stuff? In practice, you have certified compilers that are certified to actually implement the c standard correctly. >Is there something else I’m missing here, or do the nuclear systems typically have several processors that may have varying behavior for this type of stuff? You usually want to be independent of the processor (and/or vendor) used, in order to be able to easily switch it out in case they get uppity/lazy. That's one of the main uses of C.