C++ Coding Guidelines (2014)
howardhinnant.github.io
howardhinnant.github.io
Code that cryptic is exactly the kind of code I often want to modify with more instrumentation, logging, sanity checks, test cases, etcetera. "The problem seems to go away if I do X" isn't a real solution, but it can be a great hint at times, especially if you've got a small and relevant resulting difference in the disassembly. "X asserts right at the site of the fundamental bug" is handing you the solution on a gold platter. Slow build times discourage adding these things, and refactoring systems to be harder to misuse. If it takes hours to rebuild your full setup, nobody but the crazy people (such as myself) will touch common headers to preemptively add such things to make the codebase more maintainable.
Even if it's not helping you in the instant, it's helping you in the long run.
> BUCK and other build systems make compile lightning fast
It sounds like you have compile times under control wherever you are. I'm happy for you - and perhaps a little envious ;).
BUCK won't magically solve multi-minute link times. Full builds of a lot of things I've worked on require zipped, compressed, and cryptographically signed packages of gigabytes (which must then be verified, uncompressed, and unpackaged to test) - BUCK et all can't solve this terribly well either. Distributed build systems are pretty magical about solving embarrassingly parallel compiles, although even for compilation it doesn't help me too much when I'm on the bleeding edge doing hours worth of builds just to test a reasonable subset of our build configurations - maxing out a 6-core - before rolling out the latest SDKs and compilers to the rest of the build farm and my coworkers.
There are various tricks one can employ to help keep build/deploy/test cycle times in check for the common case - faster linkers like gold, distributed build systems like BUCK, dev builds which stream unsigned assets from host computers instead of from a package, etc. - but someone has to do the work of setting those all up.
In general (and for the majority of cases by a significant margin) I agree that maintainability should overrule run time performance. However as always there are tradeoffs to consider. If one is only going to use a program briefly or a small number of times maintainability becomes a lower priority.
Yes, I think it's still an issue. Let's say your project from 10 years ago took 20m to compile. Using a good build chain one could say, that's possible in 2m today, but this is still a lot of time.
Now, if it would take 2s - that would be a real improvement. One should not underestimate the vast benefits of a fast feedback loop.
Good thing you can script C++ with scripting languages like Lua to overcome this also though.
Apart from the fact that the factor is bigger (60 vs 10) it matters that if you go under 10s your workflow changes, as you don't have to work in "async"-mode anymore "oh, while this compiles I check this stuff in the documentation". For me who is not the best multi-tasker this is a real benefit.
If you're one of those "Work on the next line, keep changing it until it compiles, then keep changing it until it runs" developers, then yea you'll want fast compiles.
If your workflow is such that you take your time, do your work once, and compile/run/test as the last step before you're ready to commit, then it doesn't really matter if your project takes 20 minutes to compile. You're only doing it once or twice a day.
The problem is when I'm fixing bugs or adding small features to an existing codebase, especially when tests need to be added or modified. The compiletime turnarounds does kill my productivity in these cases.
Build time is not necessarily wasted, either - it is time to think about what comes next.
"That 2007 [Google build] took 45 minutes using a precursor distributed build system; today's version of the same program takes 27 minutes, but of course the program and its dependencies have grown in the interim ... The origin myth for Go states that it was during one of those 45 minute builds that Go was conceived."
But the result was achieved mainly by lots of compacting source code into smaller TUs, mainly, e.g. by #include-ing dozens or hundreds of .cpp files into the one that was actually sent to the compiler, and other tricks to make the number of the things the compiler had to do much smaller than a naive ("compile everything as seen in the source tree") approach would require. The original build system was not optimized in that way and took much, much longer (well over an hour, as I heard it, but that was before my time). They implemented parallel and distributed builds after I left, and as I understand it that took the whole process down to "a few minutes."
Now, it's certainly debatable whether that level of effort should be required of a build system (I lean towards "no").
For an Android build that project needs to build at least two architectures (x86 / armv7) to even function on emulator and target device.
CCache, ninja and all other stuff really helps, but we're still talking about minutes of compilation for a C++14 project.
I wouldn't put build speed above maintainability, but build speed can still be a problem for large projects.
The slowest was an 8 hour overnight build for a subset of all targets. The build would also randomly fail because of race conditions in a broken build system. I don't remember the size, but probably multiple 10s of millions of lines.
Also, it is strange for maintainability to be #4 when correctness is #1 because it is equally important for code to remain correct over time. There is nothing more frustrating than reopening issues over and over because people keep accidentally breaking things in unmaintainable code.
Absolutely. This was known far back as Ada which had reduction of errors in maintenance phase as one of its design goals for its syntax and semantics.
What would be interesting is a more elaborate discussion of the priorities based on the context (which is unknown in this case).
For example, most runtime performance is fundamentally architectural. Once you ship an architecture it is nearly impossible to change it in practice. You rarely get a second chance to do this correctly.
Correctness can be particularly insidious if the code behaves well enough to use. There are many examples of incorrectness that became a "feature" after it shipped because users started exploiting the side-effects of incorrectness in their own applications, making it very difficult or impossible to properly address the underlying broken-ness. A lot of code spaghetti is the product of a janky feature implementation that has some incorrect behavior that needs to be supported indefinitely to keep users happy. (This is what I always fear most when developing software.)
Compile times are a partly a side-effect of architecture but in practice you can often make large improvements without materially altering the design of the software. With minimal thoughtfulness in the software design, you can push this off until it really becomes painful without losing the ability to change it.
And so on. Readability and writability are among the easiest things to change after the fact.
We expect software to be more malleable. There are a lot of ways to architect software that will increase efficiency in some way but will make it harder to modify the software in the future (adding new features, closing security holes, etc). On top of that, code that's tightly tied together becomes harder to read.
Comments are great, but they've got to be maintained too, except that you don't have customers or compilers/parsers/etc enforcing that. So if a bunch of code is complicated, has a lot of inter-relationships between different pieces, etc, then part of the job is making sure that the comment is up-to-date and still correctly describes the purpose and use of the code. Clever, hard-to-read bits of code should be as small and far apart from each other as possible.
Maintainability includes more aspects than just readability.
Say that we've got that 1% case that's irreducibly complex (i.e. clarifying/simplifying the code kills the performance of something that gets run a million times a second), where we might want to explain the "how" because the code's unclear but can't be changed. The comment itself might take the form of pseudocode representing the slower-but-clearer version of the code. Being a complex and sensitive area, you'd want to have as many tests built around it as possible, to verify that the behavior doesn't change unless it was planned in advance. Part of the code-review would be that any change in behavior (as reflected by changes to the tests) would also need to be documented in the comments.
Computers are good at accurately tracking multitudes of small details (like automatically tracking all the places a piece of code is called, etc). Getting them to synthesize a summary of something's behavior ("seeing the forest instead of just the trees", the kind of thing that would be useful as a code comment) sounds like it would be interesting to research, but I don't think there's anything like that right now.
If you prioritize performance over maintainability, you're sacrificing long-term correctness, which is supposed to be your #1 priority.
PS. Also, "C++ Coding Guidelines"? This is neither coding guidelines, nor anything specific to C++.
How do I do that properly?
How do I test for undefined behaviour? I mean when I have code where I know that (ab-)using it in a certain way will trigger undefined behaviour, how do I test that?
Hm, That makes sense. Anybody know a good framework for this? I can imagine that supporting different compilers output isn't a trivial thing that one should have to rebuild themselves.
Btw: The undefined behaviour question was meant to be unrelated to the "not compiling" one.
Also this being c++, of course there are ways to test that an expression does not compile via metaprogramming (i.e. SFINAE).
I see them as tensions, the engineer's job and wisdom is in balancing these tensions
Except correctness. Code should be correct.
If you look for C++ specific guidelines I suggest this: https://github.com/isocpp/CppCoreGuidelines/blob/master/CppC...
I am saying this since C++ gives you a lot of power, but also a lot of responsibility and room for error making a more specific guide a very desirable thing.
Then, you don't use INT_MAX anymore, you use std::numeric_limits<int>::max()