Why I Still Use C++
I’ve started my career in 2000 developing C++. Since that time, I have used several other languages. But now after 16 years I still use C++ a lot. Not exclusively, but a lot. In this article I’m trying to answer the question why and when I still use C++, and also when I don’t.
The article is based on the software I’ve developed. Not sure how it applies to the software you’re building. Take it with a grain of salt.
C and C++ are very close to the metal.
No other language (besides possibly Fortran, thanks to Intel) has good support for SSE/AVX/Neon intrinsics. That SIMD thing is the only way to achieve advertised performance on modern hardware. By “advertised” I mean 8-32 FLOPs/cycle/core.
For some computation-heavy problems, GPUs are even better. They are also more limited. One limitation is algorithmic, not all algorithms are that parallel. Another one is your runtime environments, on PCs the hardware is too different, and some (especially older) embedded systems don’t have the hardware at all.
Besides SIMD, manual memory management paradigm allows implementing cache-friendly data structures; this is often hard to achieve using a higher-level language.
At the same time, modern C++ is relatively high-level language. Features like templates, lambdas and OpenMP (not part of the language but modern compilers support it on most platforms) allow high-level constructions with zero or small runtime overhead.
Technically, C runtime does exist, but it can usually be linked statically. If you have ever shipped software written in Java or C#, you probably know how the language runtime can complicate deployment and increase support costs. And for languages that compile to the native code, the runtime inflates binary size.
A well-written C++ code can be recompiled to run on almost any kind of hardware.
All OS kernels are still written in C. OS user environments are usually in C and/or C++. This means any language trying to do something useful while being general-purpose needs to implement solid interop with the code written in C.
Therefore, a third-party (think your own) component written in C++ that exposes C interface should be usable from most languages. Combined with the lack of runtime, this makes the language ideal for developing components usable across languages and frameworks.
Many programming languages have great libraries for data access, serialization, and other general-purpose things. However, in other areas, math, physics, 3D graphics and 3D non-graphics, video — C/C++ is better than the rest of them.
Sure, it has its downsides as well. Many of them are manageable, not completely but at least to some extent.
The language itself provides absolutely no safety guarantees. Especially for a beginner developer, it’s very easy to corrupt memory, leak memory, corrupt stack, and so on. Here’s some tips to manage those. I’m sure any experienced C++ developer already knows that.
- Forget about new/delete from these classic C++ books, use std::unique_ptr instead.
- For your own resources, embrace RAII pattern. This doesn’t apply just to memory management, RAII is useful for many any other things: performance measures, thread synchronization like std::lock_guard, and many others.
- Test debug builds: debug version of stl implement bounds checking for its containers. Use std::array instead of C arrays if you know the size at compile time, again, debug builds will check the boundaries.
- Design your code in a way so you know who owns what, and the memory is freed automatically. If you do need shared ownership, use std::shared_ptr for large things, and memory pools for small objects like graph nodes. Here’s a memory pool that I mostly like, MIT license.
With template-heavy code, even small project can take enormous time to compile. Template code only generates object code when the template is instantiated. Here is some counter-measure tactics.
Use templates wisely. They are extremely useful for lower-level code that’s called millions times a second. For higher-level things, C++ polymorphism and std::function are just fine.
Class boundaries are too fine grained for complex software anyway. From the architectural standpoint, you need another, higher level of granularity. You can call it components, or sub-systems, whatever. Hide those parts of your software behind minimalistic interfaces. Nothing too complex, just an abstract C++ class, and a static method returning unique_ptr will work fine for a class factory. Now, in the implementation of that component, you can use complex template-based things however you like, even boost if it brings much value to you. Because that template shenanigan ain’t part of the component interface, other parts of the software that use that component don’t need to #include that whole implementation-specific headers. You probably don’t want to do this for smaller components or individual classes, too much design-time overhead. The technique is best suited for larger parts of your software.
When writing custom template-based classes, sometimes only the API needs to be templated, because template arguments. If that’s the case, you should implement template agnostic part the classic C++ way, with the implementation in the .cpp file, and only use template methods to implement the API of your class.
Other times, you know in advance the set of template arguments you are going to need. I often deal with vectors/matrices/quaternions parametrized with ether float or double. Also with text processing code being parametrized with either char or wchar_t. If that is the case, you can place implementation in the CPP file, and use explicit instantiation with those two arguments.
A concrete example, the project I’m currently working on contains about 120k LoC in C++ language in 3 projects (two of them build static libraries used by the third one). The complete rebuild takes about 1 minute. An incremental build takes a couple of seconds. The hardware is nothing fancy, i5-4460, 16GB RAM, some Crucial SSD.
The language itself is huge and evolving. Besides, learning the language itself is not enough, to be able to develop safe and efficient code you need to learn much more things on top.
IMO, this is the C++ biggest problem. I think that was the main reason why so many people jumped into those Rust or Go bandwagons.
I don’t have any answer to that.
When I don’t use C++
Network Heavy Software
Many problems aren’t CPU bound, they are I/O bound. Higher-level languages with larger runtimes, which encapsulate e.g. libuv and provide nice high-level API on top of that, are faster than naïve C++, and much easier to use than libuv or similar C++ libraries.
There’s nothing wrong with C++ itself, QT works fine here. However, alternative platforms/frameworks are just better.
When implementing complex processing of large pieces of text, like parsing, compiling, server-side web apps, a garbage collector brings too much value to neglect.
Not all problems are performance critical. Even when they are (like mobile apps), not all problems are sufficiently performance critical to justify using lower level and less safe language.