Live data from Hacker News

Function overloading in C

gist.github.com

31–40 of 57 posts

Re: Function overloading in C

#31

This whole _Generic stuff is a mistake. There is already a descendant dialect of C which has nice overloading, namely C++. Analogy: imagine some people aren't happy with the new Fortran. Fortran 66 is the best language ever, and the only flavor of Fortran anyone should use ... except: they like a couple of features from Fortran 2008 and Fortran 90. So, they whip out the Fortran 66 standard and fork it to create "Fort…

This is like going back to y2k and saying 'well if you want double-slash comments you should use the version of c that already has it, namely c++98, and it was a mistake to add it to c'

The only way you can use double slash comments without a C version that already has them is to preprocess your code with some text filter (thereby creating your own dialect which has them).

In the 1990's, dialects of C that had // comments were C++, various dialects accepted by C compilers having // comments as a nonconforming extension over C90, and C compilers supporting draft ISO C features.

Changing dialects just for the sake of comments is silly; the right choice will usually be "don't use those comments".

In C programming, I still don't use // comments today, to keep my code strictly compatible with C90 (in my opinion, the best C dialect to date). I have been a C++ developer for many years and of course used // comments; I have nothing against them, and in fact wouldn't design a language syntax that doesn't have a "from here to the end of the line" comment feature.

(Text filtering to obtain C features is not unheard of, by the way. The CLISP sources use something called "varbrace": a text processor which provides mixed declarations and statements.)

Re: Function overloading in C

#32

Earlier quoted context omitted.

All of C's revisions retain a coherent philosophy that is incompatible with the philosophy of C++. When C is the right tool, C++ is no more a substitute than Java or C#.

C++ incorporates nearly all of C99 directly, save for VLAs, restrict, designated initializers, and a lot of small stuff that won't affect any modern-ish C code (e.g., no implicit int). VLAs and restrict are explicitly not in C++ because they're somewhat broken (C++14 did add "dynamic arrays" which are VLA-lite, and there's work on a better restrict-like functionality, cf. https://isocpp.org/files/papers/N3988.pdf ),…

> C++ incorporates nearly all of C99 directly, save for VLAs, restrict, designated initializers ...

In other words, C++ incorporates nearly all of C90, save for that C99 stuff that wasn't copied from C++! (Or not incompatibly copied.)

Re: Function overloading in C

#33

Earlier quoted context omitted.

All of C's revisions retain a coherent philosophy that is incompatible with the philosophy of C++. When C is the right tool, C++ is no more a substitute than Java or C#.

C++ incorporates nearly all of C99 directly, save for VLAs, restrict, designated initializers, and a lot of small stuff that won't affect any modern-ish C code (e.g., no implicit int). VLAs and restrict are explicitly not in C++ because they're somewhat broken (C++14 did add "dynamic arrays" which are VLA-lite, and there's work on a better restrict-like functionality, cf. https://isocpp.org/files/papers/N3988.pdf ),…

I find the lack of implicit conversion from void * really annoying. Any time I see someone cast the return type of malloc in a .c file, I curse C++ and die a little. This sounds like a trivial thing but it very much changes the feel of the language, namely into one where you need to insert pointless casts to satisfy a whiney compiler.

Re: Function overloading in C

#34

Earlier quoted context omitted.

All of C's revisions retain a coherent philosophy that is incompatible with the philosophy of C++. When C is the right tool, C++ is no more a substitute than Java or C#.

When C++ is not a substitute, it's for some reasons having nothing to do with language or philosophy, such as not having a C++ compiler for the given chip.

There are plenty of people who strongly disagree with you (Linus Torvalds being the most famous example), so you are stating a matter of personal taste, not of objective fact.

Re: Function overloading in C

#35

Earlier quoted context omitted.

C++ is not just a newer version of C with newer features. It is a completely disparate language. That's not a bad thing (tradition is not always good), but to say that if you want generics, you should just leave C behind is a little misguided.

With a couple of minor exceptions, C++ is a superset of C.

It's not just a couple. There are a large variety of things in C that Cxx does not support at all (e.g., VLAs and _Generic), and many more things that, though they are in both languages behave very differently (e.g., sizeof).

It's fine for people to like Cxx, but I do wish we could stop perpetuating the falsehood that C is just a subset of Cxx. Cxx, when it started was a pure superset of C, but that is no longer the case.

Re: Function overloading in C

#36

This is a very simple example. It only checks the type of the first parameter. _Generic can operate on all parameters, it just leads to an exponential increase of mess for each additional parameter handled. While I like _Generic in C11, there is no doubt that it is not as powerful or handy as Templates (this coming from someone who does not really enjoy Cxx templates).

It's hard to say what's more powerful. Certainly templates involve more computation, but _Generic can do some things templates can't (for good and for ill) and vice-versa.

Well, since templates are turing complete, I would tend to say that they are more powerful. But they work very differently. Templates do code generation and _Generic is just a fancy-pants type-based macro-dispatch.

Like I said, I do not think _Generic is a bad thing (though it can get really messy—which is not to say that Templates can't), just that the example given in the OP is a bit contrived.

Re: Function overloading in C

#37

Earlier quoted context omitted.

C++ is not just a newer version of C with newer features. It is a completely disparate language. That's not a bad thing (tradition is not always good), but to say that if you want generics, you should just leave C behind is a little misguided.

With a couple of minor exceptions, C++ is a superset of C.

However, with all the "great" features of C++ like templates, RAII, and overloading, programmers use and abuse every corner of them to build overly complicated ways to basically create domain-specific languages. It happens in over half of the C++ libraries I've used, otherwise they would just make their API in C.

Re: Function overloading in C

#38

Earlier quoted context omitted.

All of C's revisions retain a coherent philosophy that is incompatible with the philosophy of C++. When C is the right tool, C++ is no more a substitute than Java or C#.

C++ incorporates nearly all of C99 directly, save for VLAs, restrict, designated initializers, and a lot of small stuff that won't affect any modern-ish C code (e.g., no implicit int). VLAs and restrict are explicitly not in C++ because they're somewhat broken (C++14 did add "dynamic arrays" which are VLA-lite, and there's work on a better restrict-like functionality, cf. https://isocpp.org/files/papers/N3988.pdf ),…

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..]

Re: Function overloading in C

#39
post #38

Earlier quoted context omitted.

C++ incorporates nearly all of C99 directly, save for VLAs, restrict, designated initializers, and a lot of small stuff that won't affect any modern-ish C code (e.g., no implicit int). VLAs and restrict are explicitly not in C++ because they're somewhat broken (C++14 did add "dynamic arrays" which are VLA-lite, and there's work on a better restrict-like functionality, cf. https://isocpp.org/files/papers/N3988.pdf ),…

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 nobod…

- Destructors are very useful indeed. So is automatic reference counting: both drastically decrease the annoyance of certain styles of coding.

- 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).

Re: Function overloading in C

#40

Earlier quoted context omitted.

C++ incorporates nearly all of C99 directly, save for VLAs, restrict, designated initializers, and a lot of small stuff that won't affect any modern-ish C code (e.g., no implicit int). VLAs and restrict are explicitly not in C++ because they're somewhat broken (C++14 did add "dynamic arrays" which are VLA-lite, and there's work on a better restrict-like functionality, cf. https://isocpp.org/files/papers/N3988.pdf ),…

I find the lack of implicit conversion from void * really annoying. Any time I see someone cast the return type of malloc in a .c file, I curse C++ and die a little. This sounds like a trivial thing but it very much changes the feel of the language, namely into one where you need to insert pointless casts to satisfy a whiney compiler.

Why the hate over casting malloc? It really is not a huge issue.

A related question is why do people use malloc if calloc is available?

Post reply on HN