Why I still like C and strongly dislike C++

This comes up in my conversations surprisingly often so I thought it’s worth to write my thoughts down instead of repeating them again and again.

As it is common with C programmers, C was not my first nor my last language, but I still like it and when I have to write programs I do it in C. Meanwhile I try to be aware of modern (and not so modern) programming languages and their trends and write my own multimedia-related hobby project in Rust. So why I have not moved to anything else yet and how C++ comes to all this?

Why C is not the best language

First, the obvious statement that there is no such thing as “the best programming language”. Each language has its best use case scenarios so while you can write raytracing in Excel, it’s better be done in some other language. So it’s good to know programming language limits and do not complain that web servers are not written in Fortran and hardly any app uses Perl or C++ as internal scripting language. C may be considered not good for the following reasons (beside simply being too old and not fast-developing but that’s a matter of taste).

C has syntax that is ambiguous at the times (e.g. * may be a binary multiplication operator, an unary dereference operator, or used to declare a pointer; the fun with typedef deserves a separate essay).

It is not safe e.g. out of bounds array access is rather common and there’s no runtime check for that while e.g. Borland Pascal let alone something more modern had it (even if you could turn it off in compilation options for better performance). And the pointers make it even trickier to keep everything in order. Plus some other things like calling a function without a prototype declared so you can easily pass a wrong type of argument to it.

Rather limited standard library. Some other languages may have even web server (or at least all building blocks for one) out of the box, C standard library lacks even on containers.

Why C despite all that

The reason why I still like C is that it is a simple language. Simple in a sense that it is easy to express ideas and what to expect from it.

For example, if you want to retrieve an array value with two offsets, one of which can be negative, in C you write arr[off1 + off2] while in Rust it would be arr[((off1 as isize) + off2) as usize]. And C-style loop is often shorter than Rust idiomatic way with iterators combined together (of course the former is also allowed in Rust but it’s frowned upon with linter always suggesting to replace it using iterators). Similarly memset() and memmove() are powerful tools.

And in most cases you know what will compiler produce—what would be memory representation of the object and how you can reinterpret it differently (I blame C++ for making it harder in newer C standard editions but I’ll talk about it later), what happens on the function calls and such. C is called portable assembly language for a reason, and I like it because of that reason.

So to use car analogy, it is like a sports car with manual transmission that gives maximum performance—but you can easily damage transmission or even engine if you mess with clutch and gearbox and of course you can drive off the road if you push gas pedal too hard. Yet more of the engine power goes to the wheels compared to the automatic transmission and you can predict its behaviour and do tricks not possible on other cars (because there you’d have to fight those automated controls).

So how C++ is involved here?

And moving to C++, I don’t hate it. If you use it and like it, fine. I can’t deny that compared to C it offered two advantages: better program structuring (with namespaces and classes, Simula was good for something after all) and RAII concept (having constructors to initialise object on creation and destructors to clean it up on destruction, to put it oversimplified; if you develop this idea further you can come up with Rust lifetimes). But in the same time it also has two features that make me strongly dislike it.

First of all, it’s the holistic nature of the language. If some other programming language has a popular feature, it will end in C++ too. In result you have you have C++ standard being reviewed every couple of years with more features added every time. In result you have a monstrous language that nobody can know in full, with many features being duplicate of the other features. And everybody essentially picks a subset of C++ and writes in it ignoring the existence of other features. Plus there’s no standard way to signal features from which C++ edition you’d want to use. In Rust they have crate-wide edition. In C++ IIRC they wanted to introduce epochs for the same purpose but it didn’t take off. And a fun thought—I encounter news time from time that somebody single-handedly wrote a functional C compiler (and in reasonable time too) but I don’t remember seeing the same news about C++ compiler even once.

Another thing is that C++ is not just multiple languages in reality but also it’s a meta-language aka templates. I understand what it is for and agree it’s a better thing than C preprocessor for type-independent code. But in reality it seems to spawn hideous monstrous code including the shift from “header file for declarations, compiled code for the actual functionality” to “header file contains all the code that gets instantiated in the project including it”. I don’t like long compilation times and this approach encourages them.

And finally, I could ignore C++ existence had it not been bolted to C and giving a bad influence on it. I’m not talking about C/C++ and “you say C that means you know C++”, I’m talking about the coupling having bad influence on both standards and compilers. On one hand, C++ got a huge boost from being based off C, on the other hand it would probably be better now without most of C legacy (of course it tries to get rid of it by making it obsolete bit by bit but the legacy support is still there). But would e.g. C++24 as a separate language based in C++21 with most of outdated stuff thrown out be as popular? I doubt it.

C++ compiler influence on C

The related effect is that C gets treated as C++ without some features. The infamous case is Microsoft C-ish compiler that did not bother supporting C99 features until 2015 edition (and even then it still preferred bug-for-bug compatibility because the customers might be shocked to find out that variadic macros finally work there). But the same approach can be seen in both the standard and other compilers, and those are related issues.

The principal problem is that both C and C++ standards are written based on the input from compiler developers, and those are mostly C++ developers (and sometimes it feels they know nothing about the real-world programming and think it should just fit their views, but that’s a rant for another day). I do not follow the standard development but I’m pretty sure that the most annoying aspects in C99 and later come from those compiler developers. And those made their considerations for C++ where it makes more sense but it was forced on C as well to make the compilers easier.

I’m talking of course about “undefined behaviour” and how compilers treat it. This has become a popular scarecrow (your code relies on two’s complement arithmetic so it has undefined behaviour and the compiler can throw optimise out the whole block of code!).

In my opinion there are four kinds of behaviour that are treated as a big no-no only while half of those deserve this:

  • Architecture-defined behaviour (i.e. what depends on CPU architecture). That includes mostly arithmetic. For instance, if I know that my target machines use two’s complement arithmetic (yup, no CDC 6600) why the compiler (that is supposed to know target architecture as well) would assume that it should behave otherwise. Because then it can perform some theoretical optimisations better? Hmm… The same applies to bit shifts as well. If I know that on x86 it ignores high bits of shift amount and on ARM negative shift left means shift right why I can’t exploit that fact for my program just for that architecture? Integers having different sizes on different platforms are acceptable after all. Just issue a warning about non-portability and let me continue with it;
  • Pointer magic and type-punning. This feels forced exclusively for potential compiler optimisations. I agree that memcpy() for overlapping memory regions may not work correctly depending on its implementation (modern x86 implementations start copying from the end) and relative position of the addresses, but the other rules are less reasonable. That includes working with the same memory area using two pointers of different type simultaneously. I can’t imagine why it should not be allowed if not for hindering compiler optimisations (it can’t be an alignment issue). This culminates in impossibility of converting e.g. int to float using a union. Linus ranted on it so I should not repeat the arguments. But to me this feels like something done either for better compiler optimisations or because C++ demands it—because of the type tracking (you don’t want to put one class instance into a union and retrieve it as completely different class difference; it may do something with optimisations as well);
  • Implementation defined behaviour (here it is not exactly what C standard means by it). My favourite example is function call: depending on calling convention and compiler implementation the function arguments might get evaluated in completely random order, so the result of foo(*ptr++, *ptr++, *ptr++) is undefined and should not be relied upon even if you know the target architecture—what if they’re passed in registers (like on AMD64), the compiler is free to calculate value for whatever register it sees fit.
  • Completely undefined behaviour. This is also the case where it’s hard to argue with the standard. The most prominent example is violating the rule about changing variable state just once in single statement, like the famous i++ + i++ or even worse *ptr++ = *ptr++ + *ptr++.

Since C++ is higher-level language than C (while it has most of the features from C they are discouraged from using, you should use reinterpret_cast<> instead of direct type case and references instead of pointers, etc etc) you’d not expect C++ programmers to understand low-level code as good as C programmers (that’s just a statistical observation, of course it varies for individual people). And yet because of abundance of C++ programmers and C/C++ coupling you often have C compilers extended to support C++ and rewritten in C++ as well to accommodate for the complexity (this has happened to GCC and you have very WTFy GDB with C++ code in .c files). So you need C++ compiler to compile C compiler which is sad (but pure C compilers like LCC, PCC and TCC are still here luckily).

Conclusion

To summarise it, I like C for its middle-level position where it’s still possible to do low-level things like manipulating memory contents with ease while enjoying the benefits of high-level language (that do not get in your way); and my strong dislike for C++ comes from its design choices (though some claim it was not designed but rather simply happened) and that those design choices affect C standard and compilers making it less the language I like.

Well, at least it’s impossible to replace C90 with C90 Special Edition and pretend the original has never existed.

18 Responses to “Why I still like C and strongly dislike C++”

  1. mee says:

    Ah, what about D?

  2. Kostya says:

    That is an interesting language that (IIRC) lost its chance because of the compiler availability. I don’t have much to say about it except that now there are other languages trying to fit the same niche. As for me, they all have a right to exist.

  3. Peter says:

    Agree. Why no mention of Baidu’s new language?

  4. Matthew Fernandez says:

    FWIW some of the problems you refer to have been addressed by recent standards. C++20 mandates two’s complement integers. And type punning through a union is no longer UB as of C99.

  5. Kostya says:

    @Peter
    It does not affect C even if it came from Plan9 version of C. And I don’t know what to say about it beside that defer is a nice concept.

  6. Kostya says:

    @Matthew
    From what I can find, type punning is UB exactly in C99 (not before and not after). And in this post I wonder more about how they became problems in the first place.

  7. Joseph E Dante says:

    C was the slayer of many pop languages of the 80s like PL/1, Basic, Cobol, Pascal but eventually succumbed to the concepts of Objects in the form of C++.

    I think many of us by the 90s had our own libraries for data isolation, link list, and memory control and didn’t need fancy pants languages like Java or C++.

    I still use my old libraries to crank out very powerful programs in a few lines of code that compile and run extremely fast.

    Unfortunately, many programmers I worked with were Basic (pun intended) programmers who need to be somewhere and did have time to write very clean code, so Java was born. Java solved the memory leak problem, link lists, and data isolation so junky programmers were less junky.

    Unfortunately again, compiler writers needed to junk up another language and we have modern-day Java.

    I’m not a language guy like the author of this article, I just need to get stuff done easily and quickly and C is my best friend always. Thanks, Ritchie.

  8. Kostya says:

    I disagree that most of the listed languages died because of C.

    PL/1 was C++ of its time with the C++ problems (too many languages to chose from, too hard to write a decent compiler, maybe it’s the hardware limitations that allow LLVM to prosper now prevented us from having too complex compilers back in the day). BASIC was popular in the 1980s and enjoyed its popularity in form of Visual Basic and then Visual Basic for Apps until it was replaced by VBA2 aka Python. COBOL has never been popular with the major crowd and it’s still in use in its niche and it’s not going away from it. Pascal was intended to be an educational language (like BASIC) so while it enjoyed popularity as a systems language because of MacOS and Borland, it was still limited for larger tasks. Why Delphi didn’t keep its popularity is another question.

    As for more modern languages, Java was a product of the time—and the time demanded a safe language for less educated programmers because salaries are high and hardware became cheaper. And what do we have now for popular languages? Mostly something suitable for coders who can search what library to use in their product. As I briefly mentioned in the post, C++ seems to move in the same direction.

    Also I’m not a language guy myself, I just prefer to be aware of more than one language. Fun fact: back in university out data structures course was in C but one of the lectures was about Lisp and its “everything is a list” approach. You can program better if you know the different approaches to solving a problem.

  9. Zyx says:

    Nice sum-up, the C/C++ conflation has definitely harmed C. Any words on Clang compiler?

    COBOL has never disappeared indeed. Niche – and seemingly ill-designed – as it is, i wanna learn it. Too bad i don’t have a mainframe in the basement.

  10. Kostya says:

    Clang seems to be a side project for LLVM which was written in C++ right from the start. So the same problems with C++ mindset apply there as well.

    As for COBOL, I think you can find a compiler for something non-IBMy too. Or some zOS emulator hopefully. I understand why COBOL was designed as it is (and why it still makes sense to learn it for a retiring programmer).

  11. I agree pretty strongly with this and unfortunately C has gained an overall pretty bad rep for being complex and hard to work with. I believe this actually in 99% of the cases is due to the excessive boiler plate need to work with strings and arrays using libc.

    There are very nice and ergonomic solutions to dynamic arrays (e.g. the ones in the stb libs) and several string implementations. And once you’re using them it’s like a completely new language.

    Not to shill on my own language too much here, but I’m working on a C-like (and C ABI compatible) which is basically just C with a few GCC extensions accepted into the language and less UB. My compiler is written in plain C and I have had zero urges to switch to C++ or Java or Rust or OCaml or anything else. People really don’t know how nice the language is once you get structured.

  12. Kostya says:

    I never found that working with strings or arrays in C requires something extra. You just need to remember that string is a pointer to a zero-terminated array of bytes.

    Of course this leads to its own classes of mistakes to be made (because zero terminator is often neglected). But at least it does not matter how long string you’re processing—from “Hello, world!\n” to 6GB XML contents.

    And custom string implementation seems to be a blight of modern languages. Rust has &str for string pointer and String for mutable string (plus OsStr/OsString for platform strings), C++ is even funnier: before std::string each compiler vendor has its own string class, now you have both of these plus various custom implementations (it is joked that a large project can have half a dozen of string class implementations used by various parts of it; having seen some codebases I suspect this is not really a joke). And you have even more fun with Unicode…

    I’ve also briefly looked at C3 and it looks interesting, good luck with it and may it bring you much joy! I expect the analogy with the car still stands—not so many people like to drive and not so many cars are designed for driver’s pleasure, but there are still people who enjoy driving and either modify existing cars or build their own while others say that you need a completely different car to drive inside a city on daily basis. And both are right. The problems start when they try to make all cars to be like Opel Vectra.

  13. anon says:

    Have you seen Zig language? It could be great replacement for C. It has build-in interoperability with C libraries, so you can rewrite parts of program. It uses unified syntax for code, macro and build scripts. It has compile-time code execution. It has destructor-like defer statement, helping to not forget to clean up resources. It could have kind of polymorphism, since your can pass type as comptime: bitOffsetOf(comptime T: type,… It has for-each iterators, array slices

    https://ziglang.org/documentation/master/

  14. Kostya says:

    I’m aware of it but I haven’t seen anything convincing me that it’s a replacement for C. The fact that its compiler can compile C code as well as its own hints on rather complementing C in other areas. Its syntax is also more like Rust but simpler and with more postfix notation (while the overall approach is more like Go with defer, built-in C compiler and such).

    But let’s see what comes from it. I’m all for new programming languages trying new things, it’s just “a language for X” in my opinion is much better than “a language to replace language Y”.

  15. anon says:

    I don’t know what you consider convincing. Looking from projects https://github.com/nrdmn/awesome-zig https://github.com/topics/ziglang?o=desc&s=updated and available packages, it is a language for low-level system stuff (like barebone, embedded, uefi), to mid-level like crypto algorithms, parsers, networking, emulators etc
    As for C compiler, it doesn’t mean that it just complements, actually that’s opposite, you can replace modules in C one by one while maintaining whole program, on the other hand that means new zig code not hanging in space or requires costly marchalling, it’s immediately part of infrastructure. Oh and I forgot to mention sane error handling (almost nonexistend in C), and handy module import system. So yes, to me zig seems like perfect replacement for C.
    To be honest, C is really dated language, yes it is simple, it is easy to write compiler from scratch, parse, it’s really genius for its time, but it has downsides, C code tends to be verbose, hard to maintain, prone to bugs including vulnerabilities, on the other hand languages like C++ are too complex, they do many things behind curtains so you can’t be sure what’s going on in hardware exactly, zig aims to be that middle ground where it’s more modern and expressive than C but does not cross the line and go all wild.

  16. Kostya says:

    I’d say that part of C charm is that it handles low-level stuff smoothly, especially pointers. In Zig the usual C pointer stuff would be too verbose (it has e.g. @intToPtr for a reason) and void* is very useful for interfacing external libraries where you’re not supposed to know context type (or don’t care). And preprocessor macros have their use beside #include and #ifdef as well.

    In my opinion Zig traded some convenience of working with low-level code for better modularity, safety and other features. So it’s somewhat above C in terms of high-level—but still much closer to it than C++. It can’t replace C entirely but in certain cases it should become a viable alternative.

    P.S. I disagree that C is easy to write a compiler for, that’s Pascal. Or Forth. Or many other languages. It’s just for those many other languages you have too many features to mind and check (like C++ template substitution/instantiation or Rust borrow checker). But there are also languages like FORTRAN with its optional spaces and Raku (IIRC that can be implemented only in Raku), those are difficult to parse properly.

  17. anon says:

    I won’t argue much, but I don’t think pointers in zig would be verbose – it has various pointer types including one for C interop, and intToPtr can be wrapped in shorter helper function if that’s critical, but thing is it specially designed to be close to C and not to C++. For example it will never have operator overloading so you never have to guess will be there hidden function call. May be it stays like twice as C, while C++ is ten times further.
    Forth, I agree its easy to implement, but relatively hard to program with. But what I meant, that C standard compiler can be implemented with one person singlehandedly, and actually it was several times, like TCC, but on the other hand, implementing C++ standard looks behind one’s capabilities.
    Anyway we’ll be using whatever close to our preferences and live with it.

  18. Kostya says:

    It looks like operator overloading was such a good idea (especially for the standard I/O) that hardly any other language does that. At least in Rust where you have operations implemented as traits you have more limitations and no implicit type conversion.

    The situation with Forth is called Turing tar-pit, “in which everything is possible but nothing of interest is easy”. Still it has its uses, especially in embedded systems.

    There are different languages with different goals and application areas. Those areas may overlap with application areas of other languages and then people start to argue which language is better for the task. So it comes to the parts outside of that overlapping area people use or have to mind. As long as people are aware of the alternatives they can use whatever they prefer, otherwise you can still use hammer to put screws in, right?