Antiquated, clunky, and unsafe. Though beloved to some, C is a language that many choose to hate. The mass opinion is indeed so negative it’s hard to believe that anybody would program anything in C. Yet they do. In fact a lot of things, even new things, are programmed in C. The standard was recently updated and the tools continue to evolve. While many are quick to dismiss the language it is in no danger of disappearing.
So why do people still use C? What features does this language have that other languages don’t? Too often newer languages are so intent on avoiding C’s pitfalls they completely ignore all of its positive qualities. This ultimately leads to C’s survival simply because there isn’t much to replace it. Here I’ll look at a several of the key reasons why C is still in use, and what features it offers for system programming.
Access to the hardware
If you are attempting to work at the level of the hardware you need a language that can actually expose the hardware. You can’t have a complex a layer between your program and the underlying chips on which it is running. You often need direct access to specific memory locations, registers, and CPU instructions. This even applies to pure memory use where many concurrent algorithms require, or are best served, by a very tight control of memory.
The abstraction C offers above the machine is limited, and the layer between you and the actual machine is thin to nothing. For cases where even this is too much, C compilers tend to offer inline assembly exposing the instruction set directly to your program. These assemblers also allow access directly with higher-level C code, thus providing a very comfortable integration.
Guaranteed simple flow
I believe that exceptions are a necessity in programming any complex system. At the same time however there are situations where you absolutely do not want exceptions, such as device control and low-level concurrency. It is often beneficial to have extremely strict return handling and not worry about jumping out of the current scope (either via an exception or an implicit function call, such as a destructor or defer statement).
C also doesn’t have any VM nor garbage collection threads. The guarantees of the underlying OS are provided directly to your code. Anything which requires fine control and quick response requires a high level, if not a guarantee, of predictability. This makes resource and signal management more difficult, but at times is absolutely essential.
Clear and unambiguous
As much as I love overloading, constructors, namespaces, member functions and templates, they can, at times, leave one wondering what exactly is happening in a piece of code. Normally this obscuring of details is a bonus, but at times it can be a definite loss. If you consider the two other features, access to hardware and guaranteed flow, it only follows that you will also need to know exactly what your code is doing.
If you call a function by name you will always get the same function; the types of the parameters do not influence this. If you refer to a variable by name you either get the local function variable or the global variable. There is no chance for ambiguously matching a member variable, or one of several embedded namespace names. Operators always do the same thing. Data structures don’t have implicit initialization, or de-initialization code. Basically what you see is what you get. While at times this may be tedious it is rarely in question what function is being called or what is being modified.
A macro preprocessor
Many languages like to focus on generic reusable patterns. Instead of having to repeatedly type the same thing you can abstract into a structure which can be applied many times. This allows you to encapsulate ideas and algorithms in their essential form. It provides a nice layering which keeps the code clean and understandable. Templates, interfaces, dynamic typing, and more serve this purpose.
But often these abstractions are infuriatingly difficult, and sometimes just not possible. It is often easier, and even more correct, to just create a macro which repeats the code for you. Perhaps the overhead of the abstract structure is also unacceptable: you can’t always rely on the optimizer eliminating the redundancy. Further cases may require repetition of the declarative syntax of the language itself, and perhaps only in bits and pieces. A preprocessor resolves many such issues.
Target platforms vary greatly. When working at a low level you’ll frequently encounter situations requiring platform specific handling. Simply using a high-level “if” construct may be insufficient since some of the code blocks may not even be compilable on certain platforms. Sometimes even for a single platform you may wish to offer different builds with different features.
This could be seen as an extension of the macro preprocessor, but I consider it a very distinct and highly valued feature of C. Some languages even offer this feature without having a full preprocessor. That C allows you to intimately combine these two aspects of the preprocessor is just another benefit of the language.
Libraries and API
C is still the standard for a shared library API. If a library needs to be used by multiple projects, in possibly multiple languages, then a C header file will be provided. The C function signatures are simple and easy to understand. Importantly they are easy to support in cross-language situations. The calling convention is standard and simple to implement.The boundaries the C API imposes are very strong and thus make it suitable for shared libraries. Only fundamental and POD types are possible, thus there is no concern about how memory is managed. There are no exceptions to handle. While extremely limited it is also extremely portable.
Small to tiny binaries
While C obviously comes with a standard library, and there are thousands of additional libraries, it’s still possible to create very small C programs. First off, unlike many languages, you can actually do a lot of systems programming without even needing the standard library. Secondly, even when you do use it, you don’t have to import the entire thing into your project. C has a compile and link model that allows the linker to prune any unused code. This means that you can produce binaries that are exceptionally small. It also allows you to produce tiny binary fragments suitable for embedding.
I’m putting this last in the list of features that attracts people to C. Certainly C has the ability to produce ultra efficient and blazingly fast programs but it is not really alone in this class. Languages such as C++, Ada, and Fortran can produce equally fast, or faster code, for many algorithms. Systems have also become fast enough, and optimizers good enough, that even in low-level programming the need to do micro-optimizations is often no longer essential. While the other features of C are still required, speed is perhaps not the primary one.
Still, performance is never irrelevant and many applications can gain substantially gains not possible in other languages. Certain cache oblivious, cache aware, and concurrent algorithms actually require specific memory layouts to work correctly, or efficiently. And for situations where ever minor gain is truly helpful C provides for micro-optimization.
C is still widely used for systems programmings because there aren’t many alternatives. While C++ retains a lot of these features, it is at times difficult to avoid the higher level functionality and provide guarantees about behaviour. Nonetheless C++ is usually a good choice if you need the features that C offers. Ada needs to also be mentioned here as it is used for critical systems programming. Though not standard, common Ada compilers offer a preprocessor and inline assembler. Integration with C is also quite straight forward. So perhaps this is also a suitable combination.
So while you may still not love C, as I do not, you can certainly bemoan the lack of these features in other programming languages. While perhaps often domain specific, I can’t recall ever working on a project that couldn’t have benefited from at least a couple of the above features.