29 ms·
C Style: My favorite C programming practices (2014)
- a_e_k 2y agoI feel like I probably agree with about 80% of this. It also seems like this would apply fairly well to C++ as well. One thing that I'll strongly quibble with: "Use double rather than float, unless you have a specific reason otherwise". As a graphics programmer, I've found that single precision will do just fine in the vast majority of cases. I've also found that it's often better to try to make my code work well in the single precision while keeping an eye out for precision loss. Then I can either rewrite my math to try to avoid the precision loss, or selectively use double precision just in the parts where its needed. I think that using double precision from the start is a big hammer that's often unneeded. And using single precision buys you double the number of floats moving through your cache and memory bandwidth compared to using double precision.
- amszmidt 2y agoThe one about not using 'switch' and instead using combined logical comparisons is terrible ... quite opinionated, but that is usually the case with these type of style guides.
- aulin 2y agoit's like they purposely add some controversial rule just for engagement
- ezconnect 2y agoHe even uses 'switch' on his code.
- mcinglis 2y agoAs the author 10 years later, I agree. A hard-and-fast rule to ban switch, as that rule seems to advocate, is silly and terrible. Switch has many valid uses. However, I also often see switch used in places where functional decomposition would've been much better (maintainable / testable / extensible). So I think there's still value in advocating for those switch alternatives, such as that rule's text covers. Not that I agree with everything there either. But, useful for discussion!
- lifthrasiir 2y agoI think the fact that graphics care a lot more about efficiency over marginal accuracy qualifies for a specific reason. Besides from that and a few select areas like ML, almost any reason to use `float` by default vanishes.
- ack_complete 2y agoI'm torn both ways on the double issue. On the one hand, doubles are much more widely supported these days, and will save you from some common scenarios. Timestamps are a particular one, where a float will often degrade on a time scale that you care about, and doubles not. A double will also hold any int value without loss (on mainstream platforms), and has enough precision to allow staying in world coordinates for 3D geometry without introducing depth buffer problems. OTOH, double precision is often just a panacea. If you don't know the precision requirements of your algorithm, how do you know that double precision will work either? Some types of errors will compound without anti-drifting protection in ways that are exponential, where the extra mantissa bits from a double will only get you a constant factor of additional time. There are also current platforms where double will land you in very significant performance problems, not just a minor hit. GPUs are a particularly fun one -- there are currently popular GPUs where double precision math runs at 1/32 the rate of single precision.
- teddyh 2y agoI think you used the word “panacea” incorrectly. Judging by context, I would guess that the word “band-aid” would better convey your intended meaning.
- nine_k 2y agoA "panacea" is something that cures.every illness. 64-bit floats could do just that, in the cases listed. The cost of it may be higher than one cares to pay though. And when the cure fails to be adequate, well, it becomes a band-aid, a temporary measure in search of a real solution.
- zbentley 2y agoPerhaps "placebo" was intended?
- a1369209993 2y agoIIUC, they're using the word itself correctly, but they mean "double precision is often used with the intent of it being a panacea".
- sdk77 2y agoThere are popular embedded platforms like STM32 that don't have hardware double support, but do have hardware float support. Using double will cause software double support to be linked and slow down your firmware significantly.
- AnimalMuppet 2y agoOK, but if you're writing for that kind of platform, you know it. Don't use double there? Sure. "Don't use double on non-embedded code just because such platforms exist" doesn't make sense to me. Sure, my code could maybe run on an embedded platform someday. But the person importing it probably has an editor that can do a search and replace...
- JKCalhoun 2y ago> I feel like I probably agree with about 80% of this. What I was thinking too. There's something in here to offend everyone, and that's probably a good thing.
- projektfu 2y agoThe issue for me is that unlabeled constants are doubles and they can cause promotion where you don't expect it, leading to double arithmetic and rounding instead of single arithmetic. Minor issue, but hidden behavior.
- atiedebee 2y agoWhat's even more annoying is that the *printf functions take in double which forces you to cast all of the floats you pass in when using -Wdouble-promotion
- Lvl999Noob 2y agoI think your case comes under the "specific reason to use `float`"? If I am writing some code and I need floating point numbers, then without any more context, I will choose `double`. If I have context and the context makes it so `float`s are vastly better, then I will use `float`s.
- sixthDot 2y agofunnily the example used, i.e `printf` of single values is very special. Under the hood, variadic arguments that are `single` are actually converted to `double`. See the `cvtss2sd` in [1]. [1]: https://godbolt.org/z/Yr7Kn4vqr https://godbolt.org/z/Yr7Kn4vqr
- spookie 2y agoYeah, sometimes as a graphics programmer you don't even want the precision provided by built-in functions! As it has been pointed out though, be careful about error propagation
- liblfds-temp 2y agoDeclare all variables/qualifiers right-to-left. Read the type for all the below right-to-left, substituting the word "pointer" for "*". int long long unsigned wibble; // unsigned long long int double const *long_number; // pointer to a const double double volatile * const immutable_pointer; // immutable pointer to a volatile double They all read correctly now, when read right-to-left. It's not just "const" you do this for, as per the advice. Do it for all qualifiers.
- mrkeen 2y agoWhat's the author's justification? What's your justification? > They all read correctly now, when read right-to-left. ... suppose I'm someone who reads from left-to-right, should I flip the order to make it correct for me?
- liblfds-temp 2y agoReadability. C declarations can become unfriendly by being too complex and disordered.
- wruza 2y agoNothing seems wrong with “volatile double pointer as a constant” or “constant character pointer” either, tbh. The way you presented is equivalent, but non-idiomatic, people would stumble upon it often. To become more readable universally this must have been adopted 50 years ago.
- Someone 2y ago> What's the author's justification? What's your justification? I’m neither of them, but chances are that’s because you can’t make them left-to-right all the time. double const *foo; // foo is a pointer to a const double double *const foo; // foo is a const pointer to a double compile and do what the comment says; these do not compile: * const double foo; // a pointer to a const double named “foo” foo * const double; // foo is a pointer to a const double
- robxorb 2y ago> Write correct, readable, simple and maintainable software, and tune it when you're done, with benchmarks to identify the choke points If speed is a primary concern, you can't tack it on at the end, it needs to be built in architecturally. Benchmarks applied after meeting goals of read/maintainability are only benchmarking the limits of that approach and focus. They can't capture the results of trying and benchmarking several different fundamental approaches made at the outset in order to best choose the initial direction. In this case "optimisation" is almost happening first. Sometimes the fastest approach may not be particularly maintainable, and that may be just fine if that component is not expected to require maintaining, eg, a pure C bare-metal in a bespoke and one-off embedded environment.
- f1shy 2y agoThat was my way of thinking as I was junior programming.
- itishappy 2y agoAnd now...?
- queuebert 2y agoThey're a manager and send out daily emails reminding the coders of arbitrary deadlines.
- f1shy 2y agoWho they?!
- f1shy 2y agoAfter being burn waaay too many times with one of: 1) write only code (for the sake of “speed” 2) optimization of the wrong piece of code I do think it is much better to prioritize readability; then measure where the code has to be sped up, and then do changes, but try HARD to first find a better algorithm, and if that does not work, and more processor, or equipment is not viable or still does not work, go for less readable code, which is microoptimized
- wruza 2y agoTreat 79 characters as a hard limit Try pasting a long URL into a comment describing a method/problem/solution and you’ll see immediately that it doesn’t fit 77 chars and you cannot wrap it. Then due to your hard limit you’ll invent something like “// see explained.txt:123 for explanation” or maybe “https://shrt.url/f0ob4r” https://shrt.url/f0ob4r” it. There’s nothing wrong with breaking limits if you do that reasonably, cause most limits have edge cases. It’s (Rule -> Goal X) most of the times, but sometimes it’s (Rule -> Issue). Make it (Solution (breaks Rule) -> Goal X), not (Solution (obeys Rule) -> not (Goal X)).
- kleiba 2y agoAgree. This 80 character limit stems from a time where terminals could only display comparatively few characters in a line, a limit we haven't had in decades as screen resolutions grew. Another argument for shorters lines is that it is much harder for us to read any text when lines get too long. There's a reason why we read and write documents in portrait mode, not landscape. But in sum, I don't think there's a need for creating a hard limit at the 80 character mark. Most code is not indented more than three or four times anyways, and most if not all languages allow you to insert newlines to make long expressions wrap. However, if you occasionally do need to go longer, I think that's completely fine and certainly better than having to bend around an arcane character limit.
- f1shy 2y ago> This 80 character limit stems from a time where terminals could only display comparatively few characters in a line, a limit we haven't had in decades as screen resolutions grew. The 80 char rule has little to do with old monitors. Has to do with ergonomics, and is why any good edited and typeset book will have between 60 and 80 characters per line.
- jjgreen 2y agoExactly this. Open a novel and count the characters on a line; around 80 is readable as 500 years of typographic practice has determined. Two or three levels of indentation and that bumps the page width up a bit, still less than 100.
- deleted 2y ago[deleted]
- marhee 2y ago> developers have a hope of being able to determine which #includes can be removed and which can't Can’t a modern compiler do that already? Didn’t google but seems an obvious compiler feature at the very least behind a warning flag.
- vbezhenar 2y agoClang-tidy (linter) can do that. IMO it's a good idea to integrate this tool to any C project. I'm using gcc for embedded projects and clang-tidy works just fine as a separate tool. https://clang.llvm.org/extra/clang-tidy/checks/misc/include-cleaner.html https://clang.llvm.org/extra/clang-tidy/checks/misc/include-...
- blame-troi 2y agoI'm using https://github.com/include-what-you-use/include-what-you-use https://github.com/include-what-you-use/include-what-you-use in preference over clang-tidy.
- LeoNatan25 2y ago> 80-characters-per-line is a de-facto standard for viewing code. Readers of your code who rely on that standard, and have their terminal or editor sized to 80 characters wide, can fit more on the screen by placing windows side-by-side. This is one of the silliest practices to still be enforced or even considered in 2024. “Readers” should get a modern IDE/text editor and/or modern hardware.
- hkwerf 2y agoThe part you quoted has the one argument against yours right at the end. It's not about hardware or IDEs or text editors, it's about workspace layout.
- vbezhenar 2y agoI'm using modern IDE and 32" 4K display yet I still support this rule. One example where it's particularly convenient is 3-way merge. Also if we're talking about IDE's, they often use horizontal space for things like files tree (project explorer) and other tool windows.
- masklinn 2y agoAnd on a wide display it's very convenient to use the width to put useful ancillary content on there (e.g. docs, company chat, ...). I shouldn't waste half my display on nothing because you won't add line breaks to your code. Annoyingly lots of modern website have very wonky breakpoints / detection and will serve nonsense mobile UIs on what I think is reasonable window widths e.g. if you consider bootstrap's "xl" to be desktop then an UWQHD display (3440x1440) won't get a desktop layout in 3 (to say nothing of 4) columns layouts, nor may smaller laptops (especially if they're zoomed somewhat).
- kahlonel 2y ago[dead]
- Keyframe 2y agoau contraire! considering programming involves a lot of reading, it overlaps (or even comes from) with. best practices from ye olde tradition of typesetting https://en.m.wikipedia.org/wiki/Line_length#:~:text=Traditional%20line%20length%20research%2C%20limited,(including%20letters%20and%20spaces) https://en.m.wikipedia.org/wiki/Line_length#:~:text=Traditio.... Aside books and print magazines and newspapers, we still respect that on web sites when reading is involved, why should programming be exempt of ergonomy?
- lifthrasiir 2y agoWhile I don't agree every single point (see below), one thing great about this document is that the author tried to really elaborate one's opinion. That makes a good point to start the discussion regardless of my own opinion. Thus I'll contribute back by giving my own judgement for every single item here: Absolute agreement * Always develop and compile with all warnings (and more) on * #include the definition of everything you use * Provide include guards for all headers to prevent double inclusion * Always comment `#endif`s of large conditional sections * Declare variables as late as possible * Be consistent in your variable names across functions * Minimize the scope of variables * Use `assert` everywhere your program would fail otherwise * Repeat `assert` calls; don't `&&` them together * C isn't object-oriented, and you shouldn't pretend it is Strong agreement with some obvious exceptions * Use `//` comments everywhere, never `/* ... */` * Comment non-standard-library `#include`s to say what symbols you use from them * No global or static variables if you can help it (you probably can) * Minimize what you expose; declare top-level names static where you can * Use `double` rather than `float`, unless you have a specific reason otherwise * Avoid non-pure or non-trivial function calls in expressions * Simple constant expressions can be easier to read than variables * Initialize strings as arrays, and use sizeof for byte size * Where possible, use `sizeof` on the variable; not the type * Document your struct invariants, and provide invariant checkers * Avoid `void *` because it harms type safety * If you have a `void *`, assign it to a typed variable as soon as possible * Only use pointers in structs for nullity, dynamic arrays or incomplete types * Avoid getters and setters Agreed but you need a few more words * Don't be afraid of short variable names [if the scope fits on a screen] * Explicitly compare values; don't rely on truthiness [unless values themselves are boolean] * Use parentheses for expressions where the operator precedence isn't obvious [but `&foo->bar` *is* obvious] * Separate functions and struct definitions with two lines [can use comments instead] * If a macro is specific to a function, `#define` it in the body [and `#undef` ASAP] * Only typedef structs; never basic types or pointers [or make them distinct enough, but ISO C stole a `_t` suffix] I do so or I see why but that's really a problem of C and its ecosystem instead * Use GCC's and Clang's `-M` to automatically generate object file dependencies * Avoid unified headers * Immutability saves lives: use `const` everywhere you can * Use `bool` from `stdbool.h` whenever you have a boolean value * Avoid unsigned types because the integer conversion rules are complicated * Prefer compound literals to superfluous variables * Never use array syntax for function arguments definitions * Don't use variable-length arrays * Use C11's anonymous structs and unions rather mutually-exclusive fields * Give structs TitleCase names, and typedef them * Never begin names with `_` or end them with `_t`: they're reserved for standards * Only use pointer arguments for nullity, arrays or modifications * Prefer to return a value rather than modifying pointers * Always use designated initializers in struct literals I do so but am not sure * Write to the most modern standard you can [we have no choice for many cases] * Program in American English [only applicable for native speakers] I see why but I think you are mislead * Don't write argument names in function prototypes if they just repeat the type [such case is very, very rare] * Use `+= 1` and `-= 1` over `++` and `--` [`++`/`--` should be read as succ/pred and should be exclusively used for pointers] * Don't use `switch`, and avoid complicated conditionals [switch is okay once you have enabled enough warnings] * Only upper-case a macro if will act differently than a function call [agreed in principle, but should define "differently" more broadly] * Always prefer array indexing over pointer arithmetic [and then you will be biten by index variable types, remember `ptrdiff_t`] That's really just a personal preference * We can't get tabs right, so use spaces everywhere [as long as mechanically enforcable, the choice itself is irrelevant] * Always put `const` on the right and read types right-to-left [too eyesore] * Use one line per variable definition; don't bunch same types together [will agree with some significant exceptions though] * Never change state within an expression (e.g. with assignments or `++`) [absolutely avoid functions, but `++` has its uses] * Always use brackets, even for single-statement block [rather a read-write trade-off; this may make some codes harder to read] * Never use or provide macros that wrap control structures like `for` [the example is very tame in comparison to actually problematic macros] * Don't typecast unless you have to (you probably don't) [while many typecasts can be easily removed, excess doesn't do actual harm] * Give enums `UPPERCASE_SNAKE` names, and lowercase their values [I would rather avoid enums for various reasons] * Use structs to name functions' optional arguments [maybe the author tried to say "avoid too many arguments" instead?] * If you're providing allocation and free functions only for a struct member, allocate memory for the whole struct [that complicates using struct as a value] Just no. * Never have more than 79 characters per line [100 or 120 do work equally well, you do need some limit though] * Define a constant for the size of every enum [would imply that all enum values are sequential, and that's not true!]
- jackiesshirt 2y ago>Prefer compound literals to superfluous variables I used to agree with this but I have moved away from compound literals entirely except for global statics/const definitions. Having a variable and explicit: foo.x = whatever; foo.y = something_else; Leads to better debug experience imo, can set breakpoints and single step each assignment and have a name to put a watch on.
- flohofwoe 2y agoHmm, what's the point of single-stepping over a simple data assignment though? And when the initialization involves function calls, the debugger will step into those anyway. One advantage of initialization via compound literals is that you can make the target immutable, and you won't accidentially get any uninitialized junk in unlisted struct members, e.g.: const vec3 vec = { .x = 1.0, .y = 2.0 }; ...vec.z will be default-initialized to zero, and vec doesn't need to be mutable.
- stef-13013 2y agoVery interesting. Just one (personal) stuff : Stick to 80 columns... Sorry, no ! :)
- sph 2y ago> Always write to a standard, as in -std=c11. Don't write to a dialect, like gnu11. Try to make do without non-standard language extensions: you'll thank yourself later. Why not? Do people really care about porting their toy project to another compiler? If portability is a goal, avoid extensions, but not all projects need to be portable. I'm writing an Operating System, and I do not care it if compiles on Clang or MSVC. GCC has been around for decades, it is a safe bet.
- diath 2y agoPersonally I do, the Windows builds for my game use a Windows VM with MSVC, the Linux builds use GCC for certain optimizations, and for the dev builds I use Clang for sanitizers. Sure, you mention "a toy project" but he's talking about trying to avoid non-standard extensions in general, which is a fair point.
- 0xTJ 2y agoEven when I'm writing a toy operating system, or other toy projects, I personally always try to use the standard options like -std=c11 (though don't have anything against people who use the dialect option). I'm happy to use compiler extensions, but I'll use __asm__ instead of asm, and use __extension__ as needed. I've done some neat but truly upsetting things with compiler extensions in my hobby code, especially once I combine them with macros. I'm particularly "proud" of the mutex macros in a toy OS of mine, which wrap a statement inside for loops and switch statements for automatic release of the mutex, unless it's requested that it stay locked. There, I originally used compiler extensions to release the mutex on scope exit, but switched to non-compiler-extension code for the actual functions, and just using the extensions for checking that the code using the macros didn't break the "contracts" on what is allowed in those statements, and how they can be exited. It's the same reason that I'm always explicit about the size of integers, using stdint, even if I know that an int is 32-bit on a particular platform.
- astrobe_ 2y agoThe recommendation of using the most recent standard is very slightly inconsistent with this, because in rare cases your code could be reused in weird embedded targets that only have an unmaintained proprietary C compiler. That was already rare in 2014 I think. Still, the situation might arise, just like you can still can find AS/400 machines or Cobol in production.
- tyronee 2y ago[dead]
- gavinhoward 2y agoI agree with most, and most of the others I might quibble with, but accept. However, the item to not use unsigned types is vastly stupid! Signed types have far more instances of UB, and in the face of 00UB [1], that is untenable. It is correct that mixing signed and unsigned is really bad; don't do this. Instead, use unsigned types for everything, including signed math. Yes, you can simulate two's complement with unsigned types, and you can do it without UB. On my part, all of my stuff uses unsigned, and when I get a signed type from the outside, the first thing I do is convert it safely, so I don't mix the two. This does mean you have to be careful in some ways. For example, when casting a "signed" type to a larger "signed" type, you need to explicitly check the sign bit and fill the extension with that bit. And yes, you need to use functions for math, which can be ugly. But you can make them static inline in a header so that they will be inlined. The result is that my code isn't subject to 00UB nearly as much. [1]: https://gavinhoward.com/2023/08/the-scourge-of-00ub/ https://gavinhoward.com/2023/08/the-scourge-of-00ub/
- nwellnhof 2y agoIn the vast majority of cases, integer overflow or truncation when casting is a bug, regardless whether it is undefined, implementation-defined or well-defined behavior. Avoiding undefined behavior doesn't buy you anything. If you start to fuzz test with UBSan and -fsanitize=integer, you will realize that the choice of integer types doesn't matter much. Unsigned types have the benefit that overflowing the left end of the allowed range (zero) has a much better chance of being detected.
- gavinhoward 2y ago> Avoiding undefined behavior doesn't buy you anything. This is absolutely false. Say you want to check if a mathematical operation will overflow. How do you do it with signed types? Answer: you can't. The compiler will delete any form of check you make because it's UB. (There might be really clever forms that avoid UB, but I haven't found them.) The problem with UB isn't UB, it's the compiler. If the compilers didn't take advantage of UB, then you would be right, but they do, so you're wrong. However, what if you did that same check with unsigned types? The compiler has to allow it. Even more importantly, you can implement crashes on overflow if you wish, to find those bugs, and I have done so. You can also implement it so the operation returns a bit saying whether it overflowed or not. You can't do that with signed types. > If you start to fuzz test with UBSan and -fsanitize=integer, you will realize that the choice of integer types doesn't matter much. I do this, and this is exactly why I think it matters. Every time they report UB is a chance for the compiler to maliciously destroy your hard work.
- aap_ 2y agoFunny, this is to a great extent opposite to how I write C.
- hgyjnbdet 2y agoHow many of these are applicable to other languages?
- xigoi 2y agoProbably not many. Most of the rules are workarounds for C’s design flaws.
- mcinglis 2y agoI was surprised to see this on the HN front page, after so many years. Thanks for sharing it! Suffice to say: my opinions on this topic have shifted significantly. A decade+ more of programming-in-the-large, and I no longer pay much heed to written-in-prose style guides. Instead, I've found mechanistic "style" enforcement and close-to-live-feedback much more effective for maintaining code quality over time. A subtext is that I wrote this during a period of work - solo programmer, small company - on a green-field power system microcontroller project; MODBUS comms, CSV data wrangling. I'd opted for C primarily for the appeal of having a codebase I could keep in my head (dependencies included!). There was much in-the-field development, debugging and redeployments, so it was really valuable to have a thin stack, and an easy build process. So, other than one vendored third-party package, I had total control over that codebase's style. And so, I had the space to consider and evolve my C programming style, reflecting on what I considered was working best for that code. My personal C code style has since shifted significantly, as well - much more towards older, more-conventional styles. Still, opinionated, idiosyncratic documents like this - if nothing else - can serve as fun discussion prompts. I'm appreciating all the discussion here!
- bjourne 2y agoUpdate the text! I would love to read the diff.
- mjevans 2y agoTabs vs Spaces Tabs are always correct, IF spaces are never used instead. One tab, for one level of indent. Adjust to preference. Alas, I don't think there's a standard way of specifying... // kate: space-indent off; indent-width 8; tab-width 8; mixedindent off; indent-mode tab; Similarly, // comments should be preferred, but /* comments */ are acceptable at the top of large function blocks for large blobs of comments. Judicious / sparing use as the key idea to make it worth the exceptions if commenting out large blocks during tests or refactors.
- jdougan 2y agoThere is always .editorconfig [1] to setup indent if you have a directory of files. In places where it really matters (Python) I'll always comment with what I've used. [1] https://editorconfig.org/ https://editorconfig.org/
- kloch 2y ago> Never have more than 79 characters per line Never write lines longer than 79 characters. I'm sorry, I just cannot do this. I start to feel somewhat guity after 300 characters but 80 feels like an Atari 800.
- yoochan 2y ago"We can't get tabs right, so use spaces everywhere" I'm more like: Always use tabs, never use space. Code doesn't need to be "aligned" it's not some ASCIIart masterpiece... One tab means one indentation level and if your taste is to have tabs of pi chars wide, nice! But it won't mess my code
- pquki4 2y agoAh, seeing people posting coding styles, and other people debating them, I know my life is too short to join this conversation.
- kragen 2y agoi was sort of hoping for something like https://nullprogram.com/blog/2023/10/08/ https://nullprogram.com/blog/2023/10/08/, which shows a bunch of inventions that simplify your programming. some of them may be more trouble than they're worth, but they're at least novel and interesting by contrast, this document is largely motherhood and apple pie — and where it isn't (e.g., when it advocates titlecasing struct types or never typedeffing primitive types), i often think it's wrong. 'Write assertions to meaningfully crash your program before it does something stupid, ... to prevent a security vulnerability' is especially wrong; one of the major features of the standard assert() macro is that it's turned off in release builds! the named-arguments macro hack is an example of the kind of thing i was most hoping to find in here if you write something like this, don't dilute whatever value it may have with your opinions about tabs vs. spaces, line length, what natural language to write your comments and identifiers in, include guards, how many blank lines to put between functions, etc. these have been debated to death, and you're unlikely to have any brilliant insights about them that other people will be happy to have read