6 ms·
Rob Pike on include files [0]: > Simple rule: include files should never include include files. If instead they state (in comments or implicitly) what files t
by sergeykish 6y ago
Rob Pike on include files [0]:
> Simple rule: include files should never include include files. If instead they state (in comments or implicitly) what files they need to have included first, the problem of deciding which files to include is pushed to the user (programmer) but in a way that's easy to handle and that, by construction, avoids multiple inclusions. Multiple inclusions are a bane of systems programming. It's not rare to have files included five or more times to compile a single C source file. The Unix /usr/include/sys stuff is terrible this way.
Plan 9 compilation is very fast. It is small but also it restricts recursive #include, no guards (example [2]), each header has corresponding library, headers included in C file only [1]:
#include <u.h>
#include <libc.h>
void
main(void)
{
...
Remember Go language demo compilation time? Same authors.
[0] http://www.lysator.liu.se/c/pikestyle.html http://www.lysator.liu.se/c/pikestyle.html
[1] https://www.lysator.liu.se/c/plan9c.html https://www.lysator.liu.se/c/plan9c.html
[2] https://github.com/brho/plan9/blob/master/sys/include/bio.h https://github.com/brho/plan9/blob/master/sys/include/bio.h
- rlkf 6y agoA variant of this is to mandate that every header #define a documented symbol, and then have a #ifndef guard for this around the inclusion of the header.
- nitrogen 6y agoThe standard practice of putting the ifndef guard inside each header is specifically optimized by compilers, though I don't recall where I read that.
- jstimpfle 6y agoI don't know why this is downvoted. This is exactly how it is. The first time the compiler reads the include file, it remembers where the corresponding #endif is for each #if. The next time the file is included, the compiler can skip over the whole included file. There is no performance advantage from outsourcing the conditional to every including location. Putting an ifndef guard at the location where the file located is not workable. It's terrible boilerplate. To the relatively simple '#include <A.h>' another variable that must be synchronized with dependencies gets added. Plus 2-3 lines for the conditional. At each including location.
- cesarb 6y agoIIRC, this is documented in the CPP part of the GCC manual. Whenever a header file is completely enclosed in a ifndef guard, and that guard is still defined, it will skip the whole file. Another option is "#pragma once"; it's non-standard, but every popular C compiler (even MSVC) understands it, and has the advantage that there's no risk of accidentally using the same include guard for more than one header file.
- pjmlp 6y agoWindows compilers were the first to have it, naturally MSVC understands it.
- ncmncm 6y agoAll production compilers now support `#pragma once`, so the traditional `#include` guard is almost totally obsolete. The only plausible exceptions are in proprietary compilers for microcontroller SDKs.
- simias 6y agoI very rarely encounter #pragma once in the wild personally, and I still always use old school #ifndef guards myself. I can't really see a reason to change: it's not a huge amount of typing, it works everywhere, it's very explicit and not magical. For instance out of the top of my head I don't remember how #pragma once decides what constitutes the same include file. If you have symbolic links, or absolute and relative paths for instance, how does it work? I'm sure I can find the answer in the compiler's documentation, but with #ifndef I don't even have to, it's obvious what's going to happen. I would personally argue that old school guards should be preferred because of this and the fact that they're standard, but I don't feel super strongly about it.
- WJW 6y agoWouldn't it be as simple as maintaining a bloom filter containing the absolute paths of every included file encountered so far? Whenever you encounter #pragma once you check if the file is already present in the bloom filter and return early if it is. If it's a new file you add the file name to the bloom filter and continue processing. It probably even more memory efficient (albeit insignificant in the grand scheme of compiler memory usage) for the compiler to just maintain the bloom filter and not have to deal with hundreds of otherwise unused preprocessor symbols, one per include file.
- branko_d 6y agoBloom filter can have false positives.
- somebodynew 6y agoThe typical argument against pragma once is that it's unclear what should happen if two different hard link paths are used to include a file with the same inode (along with similar situations on Windows or with files on network drives). That is to say, in general, there is no consistent and portable "true" absolute path that uniquely identifies file(s) as conceptually having the same underlying storage. In practice, I have never actually seen a problem arise. The most likely real trigger would probably be a mix of including a library through a path that indicates a specific version and including it through a path that uses a "latest stable" link. That is already fragile and asking for trouble, though.
- john_moscow 6y agoThat is a major usability setback. It gets worse when headers A and B both require header C. Let's say your source already includes A and C and you want to add B. Now you need to manually read through the comments to understand where exactly in the include list you need to plug it. This will become completely unmanageable once your include count surpasses 20-something, which is normal for large projects.
- sergeykish 6y agoUsability is easy to fix. Replace comment with #pragma or better list dependencies in a separate file (like gemspec). Add #require keyword which resolves order automatically. That's advice from February 21, 1989. They've implemented OS and some GUI applications, for example acme text editor [1]. Maybe it is not that hard. [1] https://github.com/brho/plan9/blob/master/sys/src/cmd/acme/acme.c https://github.com/brho/plan9/blob/master/sys/src/cmd/acme/a...
- kqr 6y agoThis sounds like something that can be solved by a separate tool (similar to rcorder that determines the order in which to execute rc files in BSD systems based on their dependency specifications) one can plug into the build process.
- alxlaz 6y agoAs with much of Pike's advice, it's worth remembering where it comes from: Unix, specifically AT&T-style Unix (and later Plan 9), where a fairly disciplined (programming-wise, at least :P) team of very good, and oftentimes very experienced developers wrote a lot of small, interconnecting utilities. If your whole source code is written in that manner, and written by people who write code in that manner, this advice is easy to follow. On the other hand there aren't many files in the Plan 9 source tree that are longer than, what, 3,000 lines? 4,000? Attempting to shoehorn that, and many other well-intended nuggets of wisdom from the Unix 'heritage", into your average commercial codebase, with dozens of third-party libraries and whatnot, is unworkable (I know, I tried). Some programming advice pearls translate well into any project. "Avoid gratuitous comments like "increment i", use comments to explain the reasoning behind functionality, not describe what the source code already adequately describes" can be applied on any source tree, starting at any point. But some of them -- such as this one -- only work if you write and organize your code in a particular way. FWIW, I think that this bit of advice was useful back when Pike wrote it, in the age of slow, buggy C compilers (although some would argue we're still in that era!) munching code off of network-mounted drives. Nowadays, not so much -- I think it has readability advantages but I doubt the performance gain (if any is left) is worth the extra programmers' effort. I like to follow it when I'm writing small utilities but I don't really fuss about it for larger projects, and I've never been curious to see how fast it is. I believe (but never measured it, so take it with a grain of salt!) that it's one of those things that 30+ years of computer systems evolution (in the biological sense -- "advancement" is probably too strong a word) has made obsolete.
- raverbashing 6y agoHe's not wrong per se but this shows how utterly broken C is. And macros are a "bandage" that make the problem worse. > include files should never include include files Cool, there are two possible outtakes to this: 1) Why aren't include files generated automatically from C files then? (If you can't include anything this means they shouldn't have no "code", so they can't call anything, so they can't include anything or reference anything else) 2) Why aren't they a compilation target? (yes I know modern GCC allows you to do that) but why doesn't C specify a binary representation for them.
- pjmlp 6y agoBecause ISO doesn't force language implementers to do such tooling, which is also why there is UB to start with.
- saagarjha 6y agoAgree with the first part, but I’m not sure where you’re going with the second.
- pjmlp 6y agoThe unwillingness to standardise specific language features back in the 80s, during C89, because they would break pseudo compatible K&R compilers like Small-C, or force vendors of now long dead platforms, to actually spend a little more effort improving their toolchains.
- sergeykish 6y agoHe addressed performance issue. Just what parent asked — separate interface and implementation. Looks like you ask "why convention, not restriction?" It solved their problems while maintaining interoperation with UNIX world [1]. If convention is not enough for you, you may define format and tooling. [1] http://doc.cat-v.org/plan_9/4th_edition/papers/ape http://doc.cat-v.org/plan_9/4th_edition/papers/ape
- simias 6y agoI would strongly recommend that people new to C and reading this comment do not follow this advice. It might have made sense back when it was written but it's not how modern C is typically written, and it would be very annoying for modern C coders to put up with these shenanigans. Multiple inclusions are hardly a problem nowadays, with header guards it means that subsequent inclusions won't actually include anything and the performance impact is fairly negligible the vast majority of the time. C compilers are pretty damn fast these days and in my experience it's the optimization pass that slows things down the most, not parsing includes. Also in general I tend to take what Rob Pike says with a grain of salt, he's a very smart guy and he definitely says interesting things but he has weird idiosyncracies. He's definitely worth listening to, but I wouldn't follow his advice blindly. See his weird take about how "syntax highlighting is juvenile" for instance.
- pdw 6y agoMost modern C compilers are aware of include guards: if they notice that the _entire_ file is enclosed in include guards, they won't read it a second time.
- pjmlp 6y agoThat breaks undefs before includes.
- pdw 6y agoWell yeah. If you make normal, idiomatic use of include guards, compilers will be able to apply this optimization. If you do weird stuff, they'll be forced to re-read the file. https://gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.html https://gcc.gnu.org/onlinedocs/cppinternals/Guard-Macros.htm...
- guenthert 6y agoI haven't opened up my C compiler to verify this, but even for ancient compilers that would be a non-issue, as they see only what the preprocessor hands them. Re-reading (i.e. parsing by the preprocessor) an include file isn't so bad either, as it is already cached. I find this focus on compilation speed curious. At my last workplace a full built took a good share of the day (!) and was typically done overnight. Building during the daytime was discouraged, as it happened on our workstations, slowing down everything else. We found ways to cope, chiefly learning which subsets are autarkic, i.e. finding the least general Makefile which when invoked would catch your recent changes and wouldn't traverse too much of the tree. Not ideal, but workable.
- avodonosov 6y agoOr use a language witn normal ʼimportʼ support.