You partially countered the parent's second sentence, but didn't address the first. The real reason to use C over C++ is if you want your codebase to retain the positive effects of following C's coherent philosophy. True, as you say, C++ can be made into
almost a superset of C, and thus sticking to a small subset of C++'s additional features is a real possibility - in fact, I think it should be more common. But nobody does that, perhaps because of the difficulty of enforcement, because it would seem aesthetically messy, or perhaps because the people who aren't conservative enough to want to stick with C almost unchanged have mostly switched to C++ "proper". I don't know, but here are some of the reasons I stick with C.
My interpretation of how C benefits from its philosophy:
- Name explicitness: in C, every function in scope at the same time should have a unique name, written out in full. This means that even without much context, such as in a diff (I think there's a quote by Torvalds related to this), or with context but without advanced IDE tools, there's no confusion as to what function is being called. C++ violates this rule starting with the simple feature of method dot syntax - where the namespace for methods depends on the type of the receiver, which may be declared in some totally different location - moving on to overloads, where which overload is selected may depend on several arguments and implicit conversions and defaults and templates, which collectively can be scattered all over the codebase - never mind all the advanced stuff. The resulting code can be more succinct, but is often less clear.
Now, there may be some functions which are just so common to type, and/or which have variants acting on different types that act so similarly, that even a philosophy rejecting overloading in general may want to accept it for them. That's sort of what was for, and now _Generic lets you make your own; you don't need the entire C++ template system for that.
(In a sense, C itself violates name explicitness when it comes to struct field names, since different structs can have fields with the same name. Old fashioned C code uses globally unique names for fields, though presumably more due to feature-challenged early compilers than any philosophy. It has the benefit of making it possible to '#define my_field my_union.foo[0]' etc.)
- No implicit function calls: As you mention, destructors are pretty useful as a safety feature, and I think it would be nice to use them in C, but C++'s copy constructors and copy assignment operators and regular constructors and implicit conversions make it very easy to execute some code you didn't really want or need, without indication in the code that something expensive is happening.
For example, implicit copies caused by creating std::strings was noted last year to cause a huge number of unnecessary allocations in Chromium:
https://groups.google.com/a/chromium.org/forum/#!msg/chromiu...
- 'Mechanical sympathy': C's inability to override operators like + and [] has downsides (see below), but it does make it more clear what code lowers down to basic machine operations and what code may result in expensive algorithms being run. Compare the performance of + on integers to + on strings.
- Mechanical sympathy regarding code size: C++ templates make it very easy to bloat binary code size for minimal or negative runtime performance gain. Not that not having them at all is better, exactly, but C binaries tend to be a lot smaller...
- Simplicity -> easier to learn. Explicitness -> harder to muddle along with an understanding that's sort of right but not quite, which has upsides and downsides.
- Simplicity -> predictability for advanced users. The C spec is small enough that you can get to a point where you can read C code and almost always know what the standard says about what it should do. There are some confusing parts of the manual that get posted around on the Internet as puzzlers, like integer promotion rules, undefined behavior, sequence points, etc. - and if I were designing a language from scratch, I'd take a hatchet to these sections and pick something easier to understand. But the number of such parts is one or two orders of magnitude lower than in C++. Think function and template overload resolution rules, or the many types of construction, or the many random features which few know about because nobody uses...
- Resembles a "post-OOP" language due to being pre-OOP: these days it seems to be popular for languages to encourage things like:
-> composition over inheritance
-> using more dumb structures to store aggregate data rather than making everything a class with manually written constructors/getters/setters, hidden fields, invariants, etc.
Well, C has no inheritance, and it has long made it easy to use dumb structures. In particular, if you wanted to be able to use a struct value as an expression rather than declaring a separate variable, in C++ you had to use a constructor: 'Foo(a, b, c)' - which means that even if you didn't really want any behavior in your struct, you had to manually write a constructor to forward the parameters to the corresponding fields. In C99, you could write (struct foo) {a, b, c} without any boilerplate. Now in C++11 there is Foo { a, b, c } - which solves this problem at the expense of making initialization rules even more complicated.
And some of the biggest elements that are less about philosophy than lack of coherent design on C++'s part:
- The entire development of the template system as a metaprogramming tool, using a sort of purely functional pseudo-language, is just awful. The whole thing is incredibly complex not as a requirement to provide sufficient power, but because it evolved out of a feature set that was never intended for that use case - in fact, is famously Turing complete by mistake. SFINAE in particular is a horrible hack that makes you do things like add a default argument that will never be specified, and defaults to a class that doesn't do anything except not exist in some cases, and expect the compiler to just silently go along with this - at least concepts will improve that someday. The template system is also just not as nice as macros for a lot of code-generation-like tasks, despite the enormous amount of effort spent on it, and the standards committee's (justifiable) hate for the latter.
- Move constructors and rvalue references implement some nice functionality, but the way they're bolted on adds a ton of complexity to the type system and more boilerplate to write for your classes, and the magic that makes things like std::forward work is needlessly confusing.
- constexpr is a mess, since a large fraction of functions in general could hypothetically qualify for constexpr, but putting it everywhere would be noisy. There are other issues with it.
- Since C++ classes evolved from C structs, all the fields are specified in the declaration, which goes in the header file, even though some of them are private and logically belong to the implementation. A full list of fields is necessary in C++'s traditional compilation model if you want to make instances on the stack, but many classes are heap-only, where having sizeof be a link-time rather than compile-time constant would be no big deal. This results in unnecessary dependencies on .h files leading to longer compile times. In C you can use forward-declared structs for this in most cases; in C++, achieving "PIMPL" requires the sort of workaround code that lives up to the name.
- Poor compilation speed is mainly caused by putting everything in header files, which in turn is caused by C++ trying to retrofit features that are useful, but whose implementation would properly involve the linker - template specialization and aggressive inlining - into C's compilation unit model. Maybe modules will solve this when they're standardized in C++20 or whatever.
- Another problem caused in part by not involving the linker: the difficulty of C++ ABI compatibility on most platforms. It's naturally trickier than C due to the increased use of library types and the need to match specializations across library boundaries, but it doesn't have to be as hard as it is.
- Another is that C++'s overloadable and namespaceable function names have to go through an ABI-dependent mangling process to generate a fake C-compatible name like "_ZTVSd" - which most people can ignore, but for low-level users can be annoying.
- By the way, a knock-on effect of lack of simplicity is that the same code usually compiles slower in C++ mode than in C mode, though this isn't a big deal.
...I may as well acknowledge why I don't think C is the future:
[..continued in reply..]