Ask HN: Good C++ code bases to read?
Hi All, I managed to avoid having to write C++ for all of my career. However, especially with SDR, I find my need to write C++ and my brain is so grooved reading C code that I find it trips me up reading C++ code. Also I find I don't have a good sense of "clean" C++ code. Since I learn best by exploring, I was wondering if there are some good code bases to read where I could get a feel for what "good" C++ code would look like.
Any suggestions? Pointers to repos would be appreciated. Thanks!
264 comments
[ 2.7 ms ] story [ 254 ms ] threadIt seems that your comment follows the usual HN comment pattern. That also speaks for itself.
https://github.com/scylladb/scylla
https://github.com/catchorg/Catch2
https://github.com/facebook/folly
• https://www.scylladb.com/2020/03/26/avi-kivity-at-core-c-201... • https://www.scylladb.com/2020/05/05/how-io_uring-and-ebpf-wi...
* Classic C++ (before C++11) * Modern C++ (after C++11) * Object oriented C++ * Functional C++ * Metaprogramming (Template) C++ * I am sure there are more examples...
There is also whether you plan to use a specific library or not.
For example I have used Qt for a long time and never touched the STD (standard library) or STL (standard template library). Qt has its own Qt way of doing things.
You might need to be more specific as to which is the dommain that you want to apply C++.
It's almost impossible to use multiple libraries together without wrapping everything in one's own "house" style as a consequence.
In contrast, languages like Java and Rust, for example, have one single standard way of doing things that 99% of people just follow.
I had a quick look at the code but I would be interested in what "a style that is something between C and C++" actually entails.
Avoiding templates but using classes/namespaces? Minimizing use of the stl?
Also while ImGUI is great and the guy follows good coding practices - it's not at all what I would recommend for learning C++. Not only is it technically not C++, but the problem it solves (GUI-Framework and immediate on top) leads to some pretty hard to follow code with a lot of hidden global state.
I'm going to have nightmares tonight.
In my opinion, the very best C++ is plain-old-C that uses modern C++ standard libraries, while carefully controlling the definition of the memory allocator for those libraries.
Your idea of heavenly C++ sounds like my idea of hell.
I'm concerned this may be the definitive C++ experience...
you'd be amazed at the amount of people who start learning C++ because they want to write VSTs
The worst way is to emulate Java, putting everything in derived classes with virtual functions, and trafficking in shared_ptr. We call that Java Disease.
Using C++ as a "better C" leaves almost all the potential benefit on the table, leaving you hardly more productive than you were in C. Coding C++ as C++, you can be worlds more productive, making the compiler do most of your work for you, and leaving the bugs behind in the code you didn't need to write.
Stepanov referred to OO as "gook", and has publicly regretted making member functions like size().
But putting types to work for you is a great force multiplier.
Not to start a debate but just genuinely curious...
Do you avoid writing ~destructors() in C++ to adhere to "plain C" principles?
Imo, destructors (RAII) is one of the killer C++ features. There's a lot of buggy plain C code that has a malloc() but missing the associated free() and/or an openXyz() but missing the closeXyz(). Plain C has to manually write cleanup() functions or code generators to simulate dtors() which is a hassle.
I guess the related larger question is if the "best C++" means avoid writing any classes at all? (E.g. Linux source with plain C uses structs and function parameters to simulate OOP: https://lwn.net/Articles/444910/)
But you don't have to. If you are trafficking in unique_ptr customizations, you are coding The Hard Way. std::unique_ptr is convenient mechanism, not an organizing principle. Make a named class or class template with a member that is a unique_ptr, and traffic in instances of that class, by value.
If you are working with FILE pointers, you are doing things the hard way. Do you have some library that demands you give it a FILE pointer? Otherwise you probably want a std::filebuf. Likely on the stack, or a member of class that provides a more useful abstraction than a raw file.
`new`, much less `malloc` hardly ever need appear. I'm working in a reasonably large code base that has four uses of new: three hidden in in internal graph node construction and one in a high-performance deserializer (actually that last is in the process of being replaced by code that simply requests pages from the OS). There are very few cases where user code might allocate memory with `new` these days.
pooya13's argument stands as well.
Yes, 'automatic' variables in C or C++ are just stack allocated objects;, there's nothing about a FILE* that makes it automatic or static. But if you use, for example, streams you can just create one for a file and when it goes out of scope it closes the file and deallocates for you.
Likewise I would make a tiny class for my mmap'ed memory that did whatever cleanup (perhaps searializing data or zeroing pointers into it) before returning the pages to the OS pool. Sure you can write that code into a unique_ptr deleter and pass that in when you make them, or you could just let the compiler do it for you.
I don't use C++ in what would typically be called an "object-oriented" way, but this is a perfect example of where I would create a small class as a way of telling the compiler to do some bookkeeping on my behalf.
I was tempted to say something like "Sure, look at the best C code bases." but that would have been too snarky.
- WebKit - Chrome
Also Google's Abseil, very modern C++ patterns and containing some unexpectedly deep designs for apparently simple things. https://source.chromium.org/chromium/chromium/src/+/master:t...
https://pbrt.org/
https://github.com/mmp/pbrt-v3
That said, IMHO, there is no "clean" C++ code. There are C++ codebases that use different styles, and their "quality" more or less is context sensitive.
Personally I felt the best tutorial to C++ were actually two other programming languages - Scheme and F#.
Scheme drove in the concept that it's totally fine to model things based on the shape of the data and that you don't need to create type based abstractions around every thing.
F# then demonstrated how a language with type system is supposed to work.
The problem with C++ is that the language is so verbose that unless you have an abbreviated model in your head how a type based language can be used to solve problems in best way, you will get lost in trivial C++ minutiae.
So, personally, I generally think "Am I solving a Scheme like problem or Standard ML like problem" and then try to apply C++ as simply as possible.
Several academics have created a career of how to patch C++/Java/C# with concepts that make them less fragile in a collaborative context:
https://en.wikipedia.org/wiki/Design_Patterns
https://www.amazon.com/Design-Patterns-Elements-Reusable-Obj...
In my opinion design patterns are not a fundamental concept, but rather provide common syntax for collaboration purposes for various patterns that are parts of language and more or less invisible in e.g. Scheme or F#. But if one is diving into C++ it's probably convenient to be familiar with these concepts.
a) displays a way to put together a non-trivial C++ application
b) displays several extremely useful graphics related algorithms and patterns
c) Is documented on a level and quality few public equal code bases reach - so get the book first and then start reading the sources
Interesting - can you talk a little bit about why those specific design choices are questionable from a modern C++ POV? How would you do it differently, given the problem & domain PBRT covers?
I ask because my mental "sweet spot" in terms of understanding and writing C++ is approximately C++11. It's pretty easy for me to go into symbol shock when I see some of the latest stuff in C++17 and C++20... and I suspect I'm not the only person that happens to.
A much better approach is to allocate large chunks of memory at one time and run through the arrays of floats directly. Instead of tracing and shading one path ray all the way through, finding out where it hits, running the brdf, looking up textures, casting another ray, etc. it is much better to do each stage in large chunks.
Going from data structures holding lots of virtual pointers to something like I described above can be a substantial improvement in speed and give a lot of clarity at the same time.
Many times programs with a lot of inheritance end up with their execution dictated by types calling other types' functions, which makes it more implicit and buried rather than linear in a top level function. It becomes similar in a way to callback hell where you are trying to track down how the program gets to a certain place. Many times it takes adding break points and walking back through the call stack to reverse engineer it, rather than looking at a single top level function that has a lot of steps.
If your C++ code is "so verbose", you are Doing It Wrong, most likely by doing it the old way. C++98 was verbose.
So - verbose in terms of the spec which describes what the code actually does and mental chatter I have to do with myself when writing it "should I return a ref or a smart_ptr or a value or should I use move semantics..." whereas in F# it's just a ref and garbage collector will deal with it. So I can skip the mental chatter part.
Easy tasks:
- Challenge yourself by cloning projects reading their README and compiling/running them [1]
- Port projects to other platforms (e.g. Windows-only game examples to Linux/Mac/FreeBSD) [2]
Codebases I've studied [3]:
sdl/graphic widgets: openttd, aseprite, nanogui(-sdl)
runtime / abstraction: v8, node, protobuf, skia
templates / language integration: pybind11, boost-python
wrapping C: libsdl2pp (SDL2 wrapper)
small: tmux-mem-cpu-load, uMario_Jakowski
huge: blender
Architecture / theory: https://aosabook.org/en/wesnoth.html, https://aosabook.org/en/audacity.html, https://aosabook.org/en/cmake.html, https://www.aosabook.org/en/llvm.html
Books: Scott Meyers C++ books
[1] It's C, but to push myself to grok build systems I tried to port tmux's autotools build system to CMake: https://github.com/tony/tmux/compare/master...tony:cmake
[2] https://github.com/jakowskidev/uMario_Jakowski/pull/1
[3] https://github.com/tony/.dot-config/blob/5c59f46/.vcspull.ya...
But I would hardly call the build system the main blocker unless the software module under development is near trivial in complexity.
- http://www.serenityos.org/ (Andreas Kling)
- https://godotengine.org/ (Juan Linietsky)
Those are among my favorites because of simplicity and readability vs the complexity of the domain they implement.
positionSlider.onValueChange = [this]() { seekVideoToNormalisedPosition (positionSlider.getValue()); };
You can also capture "everything" using [&] or [=]. Handy if you don't want to make an explicit list of captured variables. You can also mix it:
Captures foo by reference and a copy of bar.https://en.cppreference.com/w/cpp/language/lambda
[0] Additionally, lambdas may not have a return value. Like this one:
The code base is quite simple and readable, because it consists of several self-contained modules, many of which are just wrappers around existing libraries.
For a "professional" code base, you could have a look at JUCE, which is also very modular: https://github.com/juce-framework/JUCE
Both projects are written in "modern C++" (C++11 and above).
Some of the older glog code is pretty nice with regards to a very vanilla and portable treatment of macros https://github.com/google/glog/tree/master/src/glog
While I wouldn't necessarily recommend Boost as a model project / repo ( https://github.com/boostorg ), it's worth checking out to help understand why modern decisions were made the way they were.
> Why did Kenton do this? He can speak for himself,
An incomplete list of reasons:
1) At the time I started the project, a lot of things that KJ replaces, like std::optional, didn't actually exist in the C++ standard yet.
2) A lot of the stuff in the standard library is just badly designed. Take std::optional, for instance. You'd think that the whole point of using std::optional instead of a pointer would be to force you to check for null. Unfortunately, std::optional implements operator* and operator-> which are UB if the optional is null -- that's even worse than the situation with pointers, where at least a null pointer dereference will reliably crash. KJ's Maybe is designed to force you to use the KJ_IF_MAYBE() macro to unwrap it, which forces you to think about the null case.
3) A lot of older stuff in the C++ standard library hasn't aged well with the introduction of C++11, or was just awful in the first place (iostream). C++11 really changed the language, and KJ was designed entirely with those changes in mind.
4) The KJ style guide (https://github.com/capnproto/capnproto/blob/master/style-gui...) adopts some particular rules around the use of const to help enforce thread safety, the specific philosophy around exceptions, and some other things, which differ from the C++ standard library's design philosophies. KJ's rules have worked out pretty well in my experience, but they break down when building on an underlying toolkit that doesn't follow them.
5) This is a silly matter of taste, but I just can't stand the fact that type names are indistinguishable from variable names in C++ standard style.
6) Because it was fun.
Do these reasons add up to a good argument for reinventing the wheel? I dunno. I think it has worked well for me but smart people can certainly disagree.
Interestingly, though, at the time I wrote the guide, Rust was in its infancy, and I didn't know anything about it. :)
(Btw, I gave a presentation about your style guide at my company two years ago, trying to convince people that we should be doing this stuff ;)
> Value types always have move constructors (and sometimes copy constructors). Resource types are not movable; if ownership transfer is needed, the resource must be allocated on the heap.
In Rust, all types (including resources) are movable.
> Value types almost always have implicit destructors. Resource types may have an explicit destructor.
What's an explicit destructor? Rust's File type closes upon destruction, and one criticism of the design is that it ignores all errors. The only way to know what errors occurred is to call sync_all() beforehand.
However, "Ownership" and "Reference Counting" (and "Exceptions" to an extent) feel very Rust-like.
> If a class's copy constructor would require memory allocation, consider providing a clone() method instead and deleting the copy constructor. Allocation in implicit copies is a common source of death-by-1000-cuts performance problems. kj::String, for example, is movable but not copyable.
When you include such a class in a larger structure, it breaks the ability for the outer class to derive a copy constructor automatically (even an explicit one, or a private one used by a clone() method). What's the best way to approach this?
Presumably not when pointers are pointing at them or their members.
In Rust, that is enforced by the compiler, but in C++ it is not. The rule that resource types are not movable is intended to provide some sanity here: this means a resource type can hand out pointers to itself or its members without worrying that it'll be moved at some point, invalidating those pointers.
> What's an explicit destructor? Rust's File type closes upon destruction, and one criticism of the design is that it ignores all errors. The only way to know what errors occurred is to call sync_all() beforehand.
I believe destructors should be allowed to throw, which solves that problem.
Obviously, this opinion is rather controversial. AFAICT, though, the main reason that people argue against throwing destructors is because throw-during-unwind leads to program termination. That, though, was an arbitrary decision that, in my opinion, the C++ committee got disastrously wrong. An exception thrown during the unwind of another exception is usually a side-effect of the first exception and could safely be thrown away, or perhaps merged into the main exception somehow. Terminating is the worst possible answer and I would argue is the single biggest design mistake in the whole language (which, with C++, is a high bar).
In KJ we let destructors throw, while making a best effort attempt to avoid throwing during unwind.
> When you include such a class in a larger structure, it breaks the ability for the outer class to derive a copy constructor automatically (even an explicit one, or a private one used by a clone() method). What's the best way to approach this?
In practice I find that this almost never comes up. Complex data structures rarely need to be copied/cloned. I have written very few clone() methods in practice.
Dereferencing null optionals is UB for consistency with dereferencing pointers. All uses of operator* should have the same semantics and the C++ standards committee did the right thing by ensuring that with optionals. Checking for null in operator* would break consistency.
If you want to dereference an optional that may be null, use the .value_or() method. For the times when you absolutely know the optional has a value use operator*.
If you’re wondering why you would use an optional over a pointer. The idea is that optionals allow you to pass optional data by value. Previously if you wanted to pass optional data, you’d have to do it by reference with a pointer. This is part of c++’s push towards a value-based style, which is more amenable to optimization and more efficient in general for small structs (avoiding the heap, direct access of data). Move semantics are a part of that same push.
In practice I believe that all standard libraries have a lightweight check mode that can assert even in release mode.
Yes, and kj::Maybe was doing the same before std::optional was standardized.
It's disappointing that the committee chose only to solve this problem while not also solving the problem of forgetting to check for null -- often called "the billion-dollar mistake".
> Dereferencing null optionals is UB for consistency with dereferencing pointers. All uses of operator* should have the same semantics
My argument is that `std::optional` should not have an operator* at all. `kj::Maybe` does not have operator* nor `operator->`.
> If you want to dereference an optional that may be null, use the .value_or() method. For the times when you absolutely know the optional has a value use operator*.
This is putting a lot of cognitive load on the developer. They must remember which of their variables are optionals, in order to remember whether they need to check for nullness. The fact that they use the same syntax to dereference makes it very easy to get wrong. This is especilaly true in modern IDEs where developers may be relying on auto-complete. If I don't remember the type of `foo`, I'm likely to write `foo->` and look at the list of auto-complete options, then choose one, without ever realizing that `foo` is an optional that needs to be checked for null.
In KJ, you MUST write:
Or if you're really sure the maybe is non-null, you can write: This does a runtime check and throws an exception if the value is null. But more importantly, it makes it really clear to both the writer and the reader that as assumption is being made.That’s fair.
>> If you want to dereference an optional that may be null, use the .value_or() method. For the times when you absolutely know the optional has a value use operator
> This is putting a lot of cognitive load on the developer. They must remember which of their variables are optionals,
Not really. Teams with programmers that are bad at keeping track of the state of their variables can simply have a policy to always use .value_or()/.value()
C++ doesn’t impose this on its users because it generally assumes its users are responsible enough to make their own policy.
> The fact that they use the same syntax to dereference makes it very easy to get wrong.
I disagree, the operator* has the same semantics as pointers did, making it no more semantically hazardous. There exists other methods on optional that have the behavior you want.
But neither of these solve the problem either. Neither one forces the programmer to really confront the possibility of nullness and write the appropriate if/else block. Throwing an exception rather than crashing is only a slight improvement IMO.
> I disagree, the operator* has the same semantics as pointers did, making it no more semantically hazardous.
It was already severely hazardous with pointers, that's the problem.
> But neither of these solve the problem either. Neither one forces the programmer to really confront the possibility of nullness and write the appropriate if/else block.
.value_or() actually does and you can certainly add a lint check against dereferencing optional or using .value() if you’d like. C++ does not Yet provide Case-style syntax for handling variants like rust, outside of macros and the standard library will certainly not define macros.
I think what you have done for your codebase makes sense based on your preferences but I think the standard optional works pretty well given the variety of code based and styles it’s intended to support.
> It was already severely hazardous with pointers, that's the problem.
So then don’t use the dereference operator.
With language support around if() and others, C++ could have mde it even more convenient. Even C could have introduced such a tyupe modifier. Whenever I read about pointers being unsafe and how optionals and maybes are the solution, I roll my eyes, because non-null-ptr do the exact same thing.
The funny thing is C++ has a non-null ptr (with no language support guarantee though): references. Unfortunately, the language made them not resettable, which makes them unusable in many scenario when you'd want them to change value over time, like in most classes members.
But the idea of a verified type can be extended by using the verified modifier on your own type. For example, you could have a verified matrix type, where the matrix is guaranteed to be valid, non-degenerate. You can apply it to:
And if teh compiler allowed the programmer to declare their own type modifier, the world is your oyster: you could for example tag that a matrix is a world matrix while another a local matrix and provide a function that converts from one to the other...I wrote a small blog post about the idea:
https://www.spiria.com/en/blog/desktop-software/hypothetical...
it's likely ~30 loc to wrap std::optional in your own type that checks for null. if std::optional checked for null and these `if` branch showed up as taking the couple nanoseconds that make you go past your time budget in your real-time system (especially if people were doing things like if(b) { foo(b); bar(b); baz(*b); }) then you have to reimplement the whole of it instead.
Don't forget that you can still use C++ on 8mhz microcontrollers.
If it gets in the way of your goal on an 8Mhz controller, take the optionality check out of your tight loop and convert to a null pointer safely where it doesn't matter.
Deeply embedded is already used to picking and choosing features, or explicitly running with the bumper rails off in the subset of cases where it matters. We like the normal primitives not being neutered for us because we still use a lot of them outside of our tight loops.
This is such a ridiculous and obviously false assertion that it’s indistinguishable from satire. Optional is widely used and was modeled from a pre-existing boost class which was itself widely used. Do you actually write C++ professionally?
And I think (hope?) that std::optional is going to make it's way into the dust bins of history like auto_ptr.
When the answer is "just don't deference it if it's null", then why not just use a pointer in the first place?
what do you mean "classic optional type"? boost.optional has worked like that for something like 20 years - it's been in C++ for longer than Maybe has been in Haskell.
Tangentially, how did you conclude that? Haskell has around since 1990 but boost only since 1999, as far as I can tell.
https://en.wikipedia.org/wiki/Haskell_(programming_language)
https://en.wikipedia.org/wiki/Boost_(C%2B%2B_libraries)
With `kj::Maybe` and `KJ_IF_MAYBE`, using the syntax I demonstrated above, you check for nullness once and as a result of the check you get a direct pointer to the underlying value (if it is non-null), which you then use for subsequent access, so you don't end up repeating the check. So, you get the best of both worlds.
> it's likely ~30 loc to wrap std::optional
It's even easier to replace std::optional rather than wrap it. The value of std::optional is that it's a standard that one would hope would be used across libraries. But because it's flawed, people like me end up writing their own instead.
I would really not call code that looks like this "best of both worlds"
when compared toThe point of optional is to avoid being consistent with the bad parts of pointers. And making it undefined rather than a guaranteed crash is even crazier.
Ford doesn't sell cars that burst into flames for consistency with the Pinto.
std::optional is for modeling a value that may be null. If the value may be null then you must check if it is null before you dereference it. There is no "forcing of branch stalls", because if used correctly (and designed correctly, which std::optional is not, sadly) it is merely a way for the programmer to use the type system to enforce the use of null checks that are necessary for the correctness of the program anyway.
If you and your coworkers find yourself in a situation in which you know that the value of a pointer cannot be null, then you should not model that value with an optional type, and then you will not only not be required to add a null check, it will be immediately obvious that you don't need one.
Hmm I think you’re suffering from a lack of imagination and real world experience with efficiently storing data in C++.
There are certainly cases where it makes the most sense to instantiate your value within an optional wrapper while at the same time there being instances within your codebase where that location is known to be non-null. I’m surprised I even have to say that.
An obvious case is when using optional as a global. Other cases are when you’ve checked the optional once at the top of a basic block to be used multiple times after.
Well, ok, although I think we were doing fine storing such values in unique_ptr. Now you're going to come back and say that you can't ever afford a double indirection when accessing globals, and if so, fine. But you still could have very easily written your own wrapper that suits your needs without demanding that std::optional be relaxed to the point where it cannot provide compile-time safety guarantees.
> Other cases are when you’ve checked the optional once at the top of a basic block to be used multiple times after.
Disagree. The way optional types are supposed to work (and the way I have used them in real code) is that you check it once, and in doing so, you obtain a reference to the stored value (assuming it the check passes). Further accesses to the value thus do not require checks. The type system is thus used to model whether the check has been done or not, and helps you write code that does the minimal number of checks required.
You seem to think everyone else in this thread is an idiot, but I promise you I have written real code with very strict optional types (similar to kj::Maybe) without introducing unnecessary branches.
You can store a reference in optional using reference_wrapper https://en.cppreference.com/w/cpp/utility/functional/referen...
I still think if you wanted to re-write a capnp library today, you'd still need kj, or at least most of it, simply for the memory control. The added benefit of kj is that you don't have to deal with C++ STL bugs and quirks. E.g. I believe C++ spec didn't require std::optional to use in-place new until recently ...
Also curious if you have any comments on this read of kj from a software management perspective. I imagine trying to sell the investment of writing something like kj at a company and it being a tough sell, even if capnp was approved. You clearly knew what you were doing from the outset and certainly nobody could have done it better. But I believe capnp sits close to the decision boundary of where many companies decide to invest in greatness or not, and reflection might shed light on why some managers make the wrong choice on something like this.
In 2013, the C++ standard library was sorely missing a string representation that allowed pointing into an existing buffer, which was important for Cap'n Proto to claim zero-copy. C++17 introduced std::string_view, which would fit the bill, but that wasn't there in 2013, so I wrote my own StringPtr. I added Maybe because I needed it all over the place (and std::optional didn't exist), and then for RPC I needed to create the Promise framework (std::future and such didn't exist yet). At that point I looked at these things and said "these are generally useful outside of Cap'n Proto, I should turn them into a library", and that's how KJ got started.
> I imagine trying to sell the investment of writing something like kj at a company and it being a tough sell
Well, many companies have things "like KJ". Google has Abseil, Facebook has Folly. But just like KJ, these things didn't start with someone saying "Hey I want to make a new C++ toolkit", they started with people writing utility code that they needed for other things, and then splitting that code out to make it reusable. Eventually the utilities accumulate into their own ecosystem. I generally don't add anything to KJ unless I explicitly need it for something else I'm working on. I actually would argue that it would be a bad business decision to spin up a project to create something like KJ or Abseil or Folly from scratch; such projects are likely to spend too much time solving the wrong problems. The best toolkits and platforms come from projects that were really trying to build something on top of that platform, and let their own real-world problems drive the design.
That said, arguably, Cap'n Proto itself is a bit of a counterpoint. I started Cap'n Proto after quitting Google. I did not have any real business purpose, I just wanted to play around with zero-copy, which I'd thought about a lot while working on Protobuf. That said, I did have the previous experience of maintaining Protobuf at Google for several years, which meant I already had a pretty good idea of what the real-world problems looked like, and I stuck pretty closely to Protobuf's design decisions in most ways. And then starting in 2014, I started working on Sandstorm, build on top of Cap'n Proto, and further development was driven by needs there. (And since 2017, Cloudflare Workers has been the main driver.)
I am not sure if the time I spent starting Cap'n Proto in 2013 would have made sense from a business perspective. If I'd wanted to start Sandstorm immediately, building on Protobuf would probably have been the right answer.
I would say that low-level developer tooling in general is pretty tough to make a business out of, because everyone expects it to be free and open source. It's also pretty tough to build as part of another business, because usually creating something new from scratch doesn't justify the cost, vs. using something off the shelf. I feel like the only people who can create new fundamental tools from scratch (especially things like programming languages) are giant companies like Google, and random hackers who are lucky enough to be able to mess around without funding.
Sorry, that probably isn't the answer you were looking for. I don't like it either. :/
Agree with you about StringPtr and string_view; also std::future; std::optional was not there and also not in-place new for a while at the start I think; lastly, I'm pretty sure unique_ptr would have been a headache over Own. I didn't really mean to suggest capnp relied on memory layout of kj types (and agree it doesnt) but rather I believe even today you'd be very hard pressed to get 100% zero-copy out of the STL.
Abseil and Folly are a lot lot bigger than KJ (folly is more of a playground), and I totally agree they are an amalgamation of utility code at team scale. KJ, though, had mainly only one author though, and it seems I got it right that capnp wouldn't be possible with the STL (at least when it started).
Wasn't so much trying to poke at the question of "does the business say KJ/capnp is necessary?" -- I agree with you that posed that way it can be hard to get a good answer.
More like: how is it best to scope out something on the scale of capnp/kj in the context of a bigger company? Do you just give a team a year and let them run?
I'm excited about capnp in the long run as more and more storage moves to NVME. Zero copy and related tricks are already big parts of Apache Arrow / Parquet; it's an important area to explore.
No, frankly, I think that would be a recipe for disaster.
The team needs to instead be tasked with a specific use case, and they need to build the platform to solve the specific problems they face while working on that use case. If you tell people to develop a platform without a specific use case, they will almost certainly build something that solves the wrong problems. Programmers (like humans in general) are mostly really bad at guessing which features are truly needed, vs. what sounds neat but really won't ever be used.
So, sadly, I think that businesses should not directly engage in such projects. But, they should be on the lookout for useful tools that their developers have built in the service of other projects, and be willing to factor those out into a separate project later.
Unfortunately, this all makes it very hard to effect revolutionary change in infrastructure. When the infrastructure isn't your main project, you probably aren't going to make risky bets on new ideas there -- you're going to mostly stick to what is known to work.
So how do we get revolutionary changes? That's tough. I suppose that does require letting a team run wild, but you have to acknowledge that 90% of the time they will fail and produce something that is worthless. If the business is big enough that they can make such bets (Google), then great. But for most tech companies I don't think it's justifiable.
some code for study are listed here: https://github.com/rigtorp/awesome-modern-cpp
It's written in Qt, but still uses "normal" C++ for a lot of stuff.
To generalize:
- Google software like protobufs and chrome are good examples of accepted C++ practice in the general software industry. It goes a bit overboard in the C++-iness for my taste, but that's just a personal thing.
- For game engines, Unreal Engine 4 is good example of pretty well-organized large C++ codebase. For size and complexity of the beast, it is not hard to read the code. Understanding the overall structure depends on how deep you are already into games.
- There are some games over the years that have released their source code. I consider Monolith's games (F.E.A.R, No One Lives Forever 2) to be pretty well-organized C++ code. They're very out of date now, but you can apply newer C++ features to their basic structure and still come away with decent code.
To learn C++, I would start with your own C code and the morph it in the following ways:
- Use C++ objects to represent functionality boundaries and data abstractions. You probably have a bunch of structs and functions that operate on them. Convert those to C++.
- Start using destructors to automatically clean up objects. Read up on RAII patterns.
- Use smart pointers instead of dynamically allocating/freeing memory with malloc/new/free/delete.
After these basics, you can branch out into exploring more in depth with templates, fancy C++17/20 features etc. Don't try to use all the features and capabilities at the outset. You can end up with a codebase you won't want to read in a year, let alone other people on your project. I've seen massive, unreadable templated code because someone got a shiny new template hammer and banged away at every problem as a nail.
I would also recommend using an IDE with decent tooling. C++ is insanely hard to parse. If you go so template/macro crazy that you confuse your IDE context parser, you will most certainly confuse other people reading your code.
And for that matter, even the same person might write in different styles depending on the situation. And the same exact style might be terrible for one situation but awesome for another.
With all that said, one "good" kind of codebase to at least know (even if you can't emulate it) is the kind of codebase that is modeled after the C++ standard library. Some Boost libraries (not all!) are great examples of this. Probably the best example I can think of off the top of my head is Boost.Container:
https://www.boost.org/doc/libs/1_74_0/boost/container/vector...
Note that this does not mean you should write code like this for your applications, though. Boost is heavyweight with the templates/headers/macros and these classes are meant to be extremely generic. Your application-level code does not necessarily need to meet the same types of constraints, and it's just not worth the effort in most cases (as well as being slower to compile). But if you can write this kind of code when it's warranted, it ends up being very high-quality. (Some parts of this involve a lot of difficult work, like paying attention to exception-safety, that is often unnecessary. Other parts are quite simple and well worth picking up, e.g. liberal use of typedefs. And everything else in between.)
A more typical example of a codebase might be some of wxWidgets, e.g.:
https://github.com/wxWidgets/wxWidgets/blob/v3.1.4/src/gener...
It's not in the style of the standard library (so you won't find it to be as generic, exception-safe, etc.) but it's pretty decent.
> https://www.boost.org/doc/libs/1_74_0/boost/container/vector...
Which I find absolutely hilarious. Thousands of lines of boilerplate for something as simple as an dynamically resizable array. Only for the _header_ which has to be included at each location that uses a vector either directly or indirectly. These are the kinds of things that prevent me from touching C++.
Please don't let boost stop you.
At my team we avoid boost like the plague and prefer terse and pragmatic code. IMO C++ is best approached like C but with convenient data structures in the standard library - and tons of patterns that _may_ be taken into use if they simplify the code.
That said, unless you are working in a specific setting where C++ is obviously the best tool, you probably should not use C++. Even at best of times C++ is complex and unproductive. But it offers an unbeatable combination of ecosystem maturity, robustness, close to the metal performance and high level concepts to fit few slots better than anything else.
I frequently sit down and write 2000 lines of C++ code, and when it compiles, it works. Aim for that.
C++ can be found to be the best language for a specific class of problems. But.
Those 2000 lines of C++? If that code had been python or F# or even C# and the problem had been 'generic' enough it would likely have less lines of code and be done faster.
If that 2k lines happened to implement some numerics stuff with hard performance requirements then C++ might come up on top.
But my complaint overall is not about a short 2k line program. It's about the whole program lifetime and software complexity of a complex system.
For non-trivial programs C++ opens a whole can of worms concerning compatibility, deployment, locales, memory handling, weird bugs due to sometimes obscure lifetimes, integer sizes on different plarforms... etc etc.
And the C++ spec is infinite for most mortals.
I've been using C++ as a daily workhorse since the early 90s and Boost and its standardized derivatives is easily the worst thing that ever happened to the language. It may all make logical sense, but the end result looks, basically, like butt. It is not a refined, thoughtfully evolved language that is a pleasure to use. It has so many features bolted on at so many different angles that it has no distinct shape anymore. Design by committee at its finest.
Boost.container::vector implements the full standard vector interface (that's already a large interface), plus quite a few non-standard extensions (significantly it has built-in support for default initialization).
Boost.container also has support for stateful allocators and custom pointer types so that it can be used, for example, on shared memory (in fact boost container was originally a sub-library of boost.interprocess). That alone will increase the complexity significantly.
It also I believe still support pre-c++11 (and substandard post c++11) compilers, so there is a lot of compiler workarounds and emulation of c++11 features.
Also, being a template, the whole implementation is just in the _header_.
As for being a template, that doesn't preclude from having most of the implementation in a .cpp file. In fact that's highly desirable (separate compilation).
A separate .cpp would require explicit instantiation. I do not see how that would work for a third party library that has to work for any T.
Normally you can at least abstract away the memory management code, but in the case of boost container the use of custom allocators is expected, so you can't even do that.
There isn't a "strictly better" here, it's a trade-off. If you want a split implementation, make one. That's the beauty of C++ - there's nothing special the standard library can do that you can't. Android, for example, has a split implementation: https://cs.android.com/android/platform/superproject/+/maste... (I wouldn't use it, though, it's pretty outdated & shitty, but you can still do what you're talking about).
honest question, why do you think liberal use of typedefs is a good habit to pick up?
That said, I'm not suggesting you use them indiscriminately. 'this_type' and 'value_type' are often useful; 'difference_type' and 'size_type' generally won't gain you anything...
It's not just that it has a huge and growing feature-set, it also has an outsize wealth of dark corners. As a slight silver lining, there's an excellent community on StackOverflow where these quirks are explained well.
You can spend hours just reading about initialization in C++. I have, and I can only recall a small fraction of it (but I rarely use C++). It's nightmarish in ways you'd never imagine. [0][1][2]
[0] https://stackoverflow.com/a/54350350/
[1] https://stackoverflow.com/a/620402/
[2] https://news.ycombinator.com/item?id=18832311
https://github.com/haiku/haiku
I would start by reading the LLVM Programmer's Manual [prog man] and the LLVM Coding Standards [code stand] and looking up how the code they reference and the examples are implemented and how they interact with each other. You can browse the code in the LLVM Mirror Git repository [llvm-git] or the cross-referenced Doxygen [llvm-doxy] (has very useful links!). If you're willing to spend the time it might be worth setting up the codebase in your IDE of choice with the proper integrations to be able to browse and cross-reference the source code there.
There is a lot to learn especially from the Important and useful LLVM APIs and Picking the Right Data Structure for a Task (and the corresponding code) in the Programmer's Manual. The data structures under llvm/ADT could be especially useful to look at. They are good generic data structures that you may even want to directly reuse in your own project, but they are much easier on the eyes and closer to what you and I would want to see in our own codebases than the source code of the standard library or the Boost data structures.
[prog man]: https://releases.llvm.org/4.0.1/docs/ProgrammersManual.html
[code stand]: https://releases.llvm.org/4.0.1/docs/CodingStandards.html
[llvm-git]: https://github.com/llvm-mirror/llvm
[llvm-doxy]: http://llvm.org/doxygen/modules.html
PS. The Coding Standards will contain a lot of subjective stuff that makes sense inside LLVM for consistency but you don't want to adopt verbatim into an existing (maybe not even a green field) project, like naming variables with Capital letters. You can skim these parts, but it might be useful to know about the existence of these types of issues.
[1]: https://twitter.com/github/status/1017094930991370240
Also on HN: https://news.ycombinator.com/item?id=17897283
PS: A fork of 2048.cpp was even used as a demo at CppCon 2020 (https://www.youtube.com/watch?v=HrOEyJVU5As)
https://github.com/abseil/abseil-cpp/blob/master/absl/string...
LevelDB is good reading, too.
https://github.com/TTimo/doom3.gpl
I find it to be extremely readable, and it has a C with classes approach that I tend to gravitate towards when I'm developing in C++. It is not an example of "modern C++".
Function is 200 lines long, with 8 or so levels of nesting. Also "goto breakout"
To address the objections below, the function reads from a file into a pixel buffer. It's not some tricky in-place update. That's a great candidate for a more functional style.
Here's more ick:
https://github.com/TTimo/doom3.gpl/blob/aaa855815ab484d5bd09...
Surely that could be factored better.
Down-voters go read Carmack's own article:
https://gamasutra.com/view/news/169296/Indepth_Functional_pr...
I think it's funny that people disagree here. This is exactly the stuff a modern linter would flag in an automated code review.
Sure enough, here's a linter. I think this is essentially the same codebase:
https://lgtm.com/projects/g/Edgarins29/Doom3/context:cpp
"Code quality: D" (on an A-F scale)
And by the way, I have the utmost respect for Carmack. I just wouldn't hold up this codebase as great.
Pure functions, or your const T& situation, actually create smaller arenas of state. Splitting out multiple functions operating on the same object does not.
> Besides awareness of the actual code being executed, inlining functions also has the benefit of not making it possible to call the function from other places. That sounds ridiculous, but there is a point to it. As a codebase grows over years of use, there will be lots of opportunities to take a shortcut and just call a function that does only the work you think needs to be done. There might be a FullUpdate() function that calls PartialUpdateA(), and PartialUpdateB(), but in some particular case you may realize (or think) that you only need to do PartialUpdateB(), and you are being efficient by avoiding the other work. Lots and lots of bugs stem from this. Most bugs are a result of the execution state not being exactly what you think it is.
in general I don't think it's worthwhile to split a long function into several static helpers just to get under an arbitrary maximum function length target. I don't think it leads to a net improvement in readability, since I now have to go back and forth between the helpers and the main function to see in what order the helpers get called (what if someone swaps the order of helperA and helperB but not the order of their definitions?). imo this is only worth doing if you're also willing to think long and hard about what happens if someone uses your helpers in a different context.
Now, C++ can halfway get there with private methods in classes. So anyone outside the class has to really try to break that encapsulation and access the function. C and C++ can get there by not exposing the functions in headers, so they remain file local.
But that doesn't prevent something like (within a file):
However, unit testing them is not the only concern. There are reasons for using nested functions, class methods, file global, or program global functions.
If you make them global or methods, you lose control of how and when they're called. This can break invariants. So some functions can be hoisted up, but others oughtn't be (in particular, any pure function can be made global without any concern other than occupying a name, side effecting functions should be more carefully considered).
The interface to the functions may change if they capture any variables. If they capture nothing in the local scope, then hoisting them doesn't impact their interface. If they do capture something, hoisting them means adding parameters (complicating the interface) or making them observe variables either in the class (complicating the class) or file/program globals (bad practice).
Regarding unit testing. Nesting functions (or lambdas) are essentially a wash. You were, hopefully, testing the host function to begin with so nothing is changed if you use nesting functions as your first pass refactoring approach. You can then examine those functions and consider which should be moved out and why, and then add tests to any that have been pulled out of the host function.
If the functions are capturing a lot of variables, then I would try to reconsider the design. Usually things aren't irreducibly complex.
I don't exactly follow your last paragraph, but tests aren't just something you throw away when they pass. So if I were to write tests for the helper functions, I wouldn't delete those tests on a refactoring pass in order to use nested functions.
If tests can do this, then so can anyone else. Consequently encapsulation is broken and your invariants aren't invariant anymore.
> I don't exactly follow your last paragraph, but tests aren't just something you throw away when they pass. So if I were to write tests for the helper functions, I wouldn't delete those tests on a refactoring pass in order to use nested functions.
I didn't write clearly because I didn't re-present the context of that paragraph.
I'm not talking about throwing away tests after they're run. Keep in mind my original post's context: manually inlined functions for access control to that functionality. You already can't test those separately because they aren't exposed. By moving to nested functions you regain some semblance of reasonability (versus 1k+ line functions with who knows how many levels of nested blocks) and the compiler can do the inlining (for performance). But it has zero net effect on testing, it's a wash. Because the public interface is the same (only the primary function interface is accessible to a tester).
If nested functions are available (and with C++ they are with lambdas) the refactoring would (or could) be something like: 1k line function => 500 line function with several lambdas => 3-8 functions totaling ~500 with some lambdas remaining.
Only those that make sense to move out for separate testing would be, and only if you also wanted to expose them for others to call.
If I had an image loader class, I can make a test function a friend, which would allow it to call private functions on the class. This only grants access to the test function (or test class). And there are stronger ways to hide things, like the PIMPL idiom, private headers, opaque pointers.
I find it interesting we're in such different schools of thought here.
> Only those that make sense to move out for separate testing would be, and only if you also wanted to expose them for others to call.
There are so many things that you might want to hide from an interface, yet still test. Imagine if you took that to an extreme and only tested the public interface of a library. I'm all for trying to independently test any bit of code that fills a screen.
Personally, I just find it frustrating, if functions don't fit on the screen anymore (and I do use portrait mode already). Further, sub functions, when named appropriately (definitely not like helperA and helperB) can aid readability (as would comments about code blocks do, but who writes those and who maintains those?).
[1] https://stroustrup.com/JSF-AV-rules.pdf
Could they have abstracted MakeMegaTexture_f into something that built up leaves of tga structs they interweave in that function? Or just chunk through them with the tga data structure. Our standards for what is good code has changed with our understanding of code.
The code is readable and self documenting. The file format is practically documented by reading this code. The function has a single obvious purpose.
The function length and nesting is more a result of the file format itself. Seems like a waste of time breaking this function up. It would serve no other purpose than delaying the ship date and making this function less readable, a function that may likely never need to be visited again.
Moreover, here's approximately how I'd write that TGA loading function:
1. Write a function to load just a header. 2. Unit test the function with a header. 3. Write a function to do the RLE decoding. 4. Unit test the function with some data. 5. etc. 6. Start assembling the pieces. 7. Unit test the whole thing.
Meanwhile, you tried to write it all in one monolithic function, and so now you're testing the whole thing (hopefully with a unit test) and you're staring at the debugger (or worse, some printf output) wondering what little mistake you made. Maybe if you're brilliant, like Carmack, you beat me to the finish line. But most of us are mere mortals.
Can you rewrite it in a way that's readable, performant and understandable/maintainable to someone with an understanding of the knowledge domain?
I agree, the code is ugly. I disagree it would be better if converted to some OOP hierarchy, it would be probably less understandable even if the code was tidier
Code like
Is tidy but absolutely denseSimple code is easy to write, maintain, and optimize later. Despite the code looking messy, I find it easy to understand and navigate.
A counter example of truly confusing code, which kind of does similar things, would be something like this: https://github.com/ImageMagick/dcraw/blob/master/dcraw.c
Anyway, he's just wrong about long functions being a-ok. I suspect he doesn't write many unit tests. This is the main benefit of separating the code into smaller functions. You can test each. Even if it's just parsing an image file header.
That some of the folks here think smaller functions are useless because they would only be called from from one function just shows they don't write enough tests.
Splitting everything up and testing separately, make sense if you are building a library or general user program. For program where you control whole toolchain its overkill.
You need to be able to read correct image and hopefully not segfault on bad one. And this function does that.
Modern C++ and it's use of std::unique_ptr / std::move is so much nicer vs. manual memory allocation.