Counterargument that destroys your entire premise: C's revisions( note that revisions are not flavors or separate languages ) don't fork.
Counterargument that destroys your entire premise: C's revisions( note that revisions are not flavors or separate languages ) don't fork.
Is that so? My apologies. What argument have I reduced to a strawman, and what is the proper strong version?
> note that revisions are not flavors or separate languages
By what definition of "separate"?
C11 is in fact a different language that is very similar (and explicitly based upon) C99, and the same holds between C99 and C90.
These can be said to be dialects. But by the same token (no pun intended), C++ is also a C dialect.
When C is the right tool, assuming that it ever truly is, C++ is a vastly better substitute than Java or C#. C++ uses the same machine model as C; Java and C# do not.
In practice, then, C++ without RTTI, exceptions, or the standard library is basically a strict superset of C and C code compiled as such will tend to be legal and identical in semantics. The same cannot be said for C# or Java. Given that C++ has some rather useful features (most notably, the ability to guarantee cleanup even in the face of inattentive developers), the only real valid reason to not be using C++ instead of C is if there's no C++ compiler.
In other words, C++ incorporates nearly all of C90, save for that C99 stuff that wasn't copied from C++! (Or not incompatibly copied.)
A related question is why do people use malloc if calloc is available?
But more stylistically, let's be honest, something like this is way too verbose:
struct foo *bar = (struct foo*)malloc(sizeof(struct foo));
I see code like this often, and I think the only explanation I can think of is that people don't know the language well enough to write: struct foo *bar = malloc(sizeof(*bar));
> A related question is why do people use malloc if calloc is available?Seems like a very different question... calloc multiplies, calls malloc, and zeros. I guess if you don't want to multiply, don't want to zero, don't want to bother specifying another parameter, some combination of these, then malloc might be a better choice. Though I do recall a few years back the OpenBSD folks (possibly others too?) focusing on making sure calloc checks for overflow when it multiplies. [See also: reallocarray]
I was under the impression that lots of implementations of malloc actually call calloc under the hood. Unless you are calling malloc in a tight loop I can’t see the extra step of zeroing being a issue. In one of my programs where performance is critical I changed over all my mallocs to calloc and couldn’t measure the time change - I did find I had fewer difficult-to-duplicate bugs though.
Edit. Interestingly CERT recommends casting [1]. Seems to solve a much worse issue than forgetting to include stdlib.h
1. https://www.securecoding.cert.org/confluence/display/c/MEM02...
GCC also has diagnostics for when a function definition occurs without a declaration.
From GCC man page:
-Wmissing-prototypes (C and Objective-C only)
Warn if a global function is defined without a previous prototype
declaration. This warning is issued even if the definition itself
provides a prototype. The aim is to detect global functions that
fail to be declared in header files.
-Wstrict-prototypes (C and Objective-C only)
Warn if a function is declared or defined without specifying the
argument types. (An old-style function definition is permitted
without a warning if preceded by a declaration which specifies the
argument types.)
-Wimplicit-function-declaration (C and Objective-C only)
Give a warning whenever a function is used before being declared.
In C99 mode (-std=c99 or -std=gnu99), this warning is enabled by
default and it is made into an error by -pedantic-errors. This
warning is also enabled by -Wall.Firstly, anyone who does mallocs a block, and then immediately does a memset(block, 0, size) on that block almost certainly should be using calloc. The Linux kernel has kzalloc for this purpose as an alternative to kmalloc and it's fairly widely used.
Sometimes it is inefficient to set something to zero, when that is not the true initial value (some or all of the zeros will be overwritten with the true initial value before the object is used). If you malloc some structure and then painstakingly initialize every member, then calloc is wasteful.
If you malloc some structure, and then forget to initialize some of the members, calloc will hide that bug by initializing them to all zero bits. This might not be the correct value. If the uninitialized parts of the structure aren't clobbered from zeros, you can get an accurate diagnostic about that from a tool like Valgrind. On the other hand, if you aren't using that tool, the behavior with zeros is more deterministic. E.g. if a pointer is not initialized, but overwritten with zeros, on most mainstream platforms that will be a null pointer whose dereferencing uses are detected. If you don't have such a tool available, having the uninitialized bits be all zero is arguably better.
Much of this reasoning mirrors reasoning about whether to initialize a local variable to zero, if its true initialization actually happens later.
Finding bugs where you write 101 bytes are easy to find with tools like Valgrind, but finding bugs where you malloc and don’t initialise the data structure correctly are a real pain.
You could use the exact same argument against any type-safety feature: inserting "pointless" forward declarations, inserting "pointless" const qualifiers, etc., to "satisfy a whiney compiler."
The thing is, implicit conversion from void* is plainly unsound, type theoretically speaking. It has certain advantages, but is not necessary in order to have those advantages. For example, it makes use of malloc easier, but one could also make the vast majority of uses of malloc easier with something like
#define ALLOC(TYPE,N) ((TYPE*)malloc(N*sizeof(TYPE)))
Or at least, one could if C had a remotely regular type syntax and/or actual type genericity. In contrast, you actually can write template <typename T>
T* alloc(std::size_t n) {
return static_cast <T*> (malloc (n * sizeof (T)));
}
in C++, and it's even straightforward to add additional logic like overflow detection. Mark it inline and stick it in a header, and it's effectively the same as the above ALLOC macro, except that it actually works. Now, contrast int* array = malloc(256*sizeof(int));
with int* array = alloc<int> (256);
The C++ expression is simpler. Now, if you change the type of the array, but forget to change the allocation expression (for whatever reason; maybe you're using the absurd convention of declaring all your variables before you start initializing and using them): char* array = malloc(256*sizeof(int));
// vs
char* array = alloc<int> (256);
You get a type error with the C++ version, and the C version silently compiles. Not too much of an issue in this case -- you'll just waste a bunch of memory -- but go from char to int instead of int to char, and now you've got buffer overflows.As for the argument made elsewhere in this thread that explicitly casting the result of malloc in C can mask the fact that malloc has been implicitly delcared, that's a language bug -- one that was fixed more than fifteen years ago.
In all seriousness though, I really mean it when I say I intellectually understand the C++ fan's response to a number of these issues. It took me a long time of writing C but I do understand the template-nerd habitat, and can acknowledge its strengths and use those tricks when working in a .cpp file. What frustrates me is to see the rigidity with which it is followed, and the failure to understand the world in a different way. There is a different world out there that doesn't mind an implicit conversion from void pointers, and they're not "wrong".
>What frustrates me is to see the rigidity with which it is followed, and the failure to understand the world in a different way. There is a different world out there that doesn't mind an implicit conversion from void pointers, and they're not "wrong".
I have seen and lived in the world where casts from void* are common, and where making those casts implicit is a convenience (and that world is not the world of C++). I understand that world. I understand that viewpoint. I just strongly disagree with it.
Very well aware of the C++-head's answer to these problems.
> My point is that the lack of implicit casts from void* should not be frustrating,
This was a discussion about whether or not C++ is a superset of C. I wrote up the difference that frustrates me the most and all other comments were missing.
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 <tgmath.h> 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..]
- Re: 'mechanical sympathy', encouraging raw pointer access and such is nice until you start caring about security, at which point the few extra instructions required to bounds check vector accesses are infinitely less problematic than the alternative; in C++ you can stick them in vector::operator[], not in C.
- C makes it extremely difficult to write efficient container types, due to the lack of templates. It's possible using macros, and I've done it, but the contortions and pitfalls inherent in the implementation of such macros eliminate any kind of aesthetic benefit. This encourages things like the use of raw {pointer, size} pairs over specialized types, which requires obnoxious boilerplate, and dumber data structures rather than smarter (e.g. linked list vs. hash table, when the number of entries is expected to be small - until suddenly it isn't).
- On a related note, templates in general, when used right, make it easy to generate highly efficient specialized code, useful not just for containers but in many situations. It's a blemish to C's reputation for speed that it has more difficulty doing that.
- Both languages lack proper sum types, which are a natural counterpart to structs; unions are nice but explicitly tagging them is entirely gratuitous unsafety.
- Sometimes you really just want short code, and not to have to cross every i and dot every t. UI frameworks are a particularly good example, where there are a large number of knobs to turn, and many more that should be left at their defaults, making it very desirable to (a) minimize the length of code required to turn one and (b) make it as easy as possible to leave things unspecified - encouraging the free use of default parameters and overloads, and making the repetition involved in global name uniqueness look pretty ugly. I think this paradigm can be improved upon in general, but I'm not sure C is the language to do so in.
Still, I don't think C++ is the future either, not even the improved-but-further-bloated newer versions. Nor do any of the other languages in the market do a good job at targeting the niche C currently excels at (including Rust).