Why I should have written ZeroMQ in C, not C++ (2012)
250bpm.com
250bpm.com
https://news.ycombinator.com/item?id=3953434
Then, the original author wrote a follow-up which had another ton of comments on his blog and another 162 comments on HN.
https://news.ycombinator.com/item?id=4455225
I wonder if anything new can be said here.
Still, you will be able to do "pretty darned well" in Go. And there's a lot of use cases for which that will be indistinguishable from the "absolute top end".
http://www.azulsystems.com/technology/java-garbage-collectio...
So yes, it is possible to have pauseless GC. That won't fit all problems that C++ is used for (like embedded), but it certainly hits on the server software side of things.
Most modern server software performance is significantly bound by memory bandwidth and latency. Introducing inefficiencies in this area tends to result in material degradation of software throughput. In fact, this is the only substantial advantage of C/C++; it can be efficient with memory access, caching, and utilization in a way that no GC language can be, and these are major contributing factors to performance.
I'd actually say quite the opposite of what you are saying: in a server environment, you often care more about throughput and mean latency (as opposed to 99th %-ile latency), and GC can really do great there. What's nice is that GC allows you to have near zero cost heap allocations and amortizes the cost of cleanup. What tends to be the problem is a lot of languages with GC make it difficult to efficiently work with the systems's buffer cache, so you typically see problems with systems that do disk I/O with large amounts of data.
I've written a LOT of C/C++ & JVM based servers. Particularly for large projects with a lot of independent moving parts and object lifecycles that are not tightly tied to sessions, the JVM based servers are remarkably competitive if they aren't disk IO bound.
There's a reason why a lot of HFT firms use Java...
C/C++ still kicks butt for tightly written servers with fairly fixed functions, and generally for anything that does tons of disk IO (though the Cassandra guys have, as an example, demonstrated one can do quite well if you take the time to get it right), and of course anything with unsigned arithmetic (seriously, when are we going to acknowledge that this was a bad idea?).
It may not be "inherent" to GC, but in practice every GC that I'm aware of has terrible cache efficiency compared to manual memory management.
> Copying collectors (like the one the HotSpot VM uses for Eden Space) tend to have great cache efficiency as compared to typical use of the C heap
False comparison. The real test is against high performance memory techniques (like memory pools, which are relatively easy and common in practice if you care at all about cache efficiency) not "typical use of the C heap." When you actually compare GC vs high performance manual memory management, manual memory management is significantly faster.
That's not a false comparison. They represent about equal development effort (well, the GC version is likely still less effort).
Sure, if you spend a ton of time optimizing your memory usage for your specific needs you will do better than a general purpose algorithm (though it is remarkable how often those solutions often implement their own forms of automatic memory management tuned for their needs). If it turns out that you couldn't do that, it'd be awfully hard to collect a paycheck (and in fact most programmers could not collect that paycheck).
Of course, even that effort would not measure up as compared to an omniscient GC algorithm (and the paycheck for building that would be even bigger ;-). What is the point?
GC's do well. When you really need to tune memory usage, you get in there and do the work, and you and do that in GC'd runtimes too. A good memory manager will, for common workloads, start you off in a better place and make the work of tuning easier. Certainly there are cases where it won't be so helpful, but that doesn't mean they are "bad" at memory cache efficiency.
Hmm. Clearly I don't know much about this. How do you turn off garbage collection in Java and (say) Python?
BS. Most languages don't allow you to turn off their GC, except if by it you mean doing silly tricks and jumping through hoops, using tons of memory to avoid having to GC, using pools of pre-allocated objects, not using any standard libs, etc. Even then, they are not callable from outside code (like C is and Rust tried to be) and memory use is huge.
"Tried"? AIUI, every part of Rust is a work-in-progress.
Pretty much every language can interoperate with C.
Broken record here: there is a middle ground between importing a "3rd party" library (usually this means GLib) and writing your own Red-black tree: you can just port a lot of the STL back to C. I did that 10 years ago to get a workable straight-C RB tree by specializing STLPort's <map> on void-star. It's been tremendously helpful.
Another idea that more C code should adopt from STL is <vector> --- making a thin wrapper around resizable arrays the go-to container, rather than linked lists which are the de-facto go-to today.
However after playing around with the idea in a few C++ modules I was writing I came to the conclusion that it's just not worth it. Part of what makes C so enjoyable to write is its relatively "weak" typing and the flexibility that comes with that, primarily freely casting between pointer types including void*'s where necessary. Templates fight against that at every opportunity and I found myself spending more time trying to figure out how to wrangle my code into the template type system than actually making progress on the problem at hand. Newer additions like C++11 constexprs take that type-system frustration to an entirely new level.
Generics. Polymorphic functions and collections don't require the complexity of C++ templates.
void *p = /* ... */;
char *q = p;
Despite what the C++ weenies will say this is valid C! Please do not commit this cardinal sin: char *p = (char*)malloc(n);
No no no! No need to cast that! Please just write: char *p = malloc(n);
And if somebody tells you that won't compile without an explicit cast, please kindly remind them what language you are writing.[Actually I agree with you that C++ templates are really nice, chiefly for the reason that they allow you to avoid function pointers. In C++ you can pass your "callback" to a template and it gets inlined right with the body of the function.]
The compiler is of course perfectly happy when it's running in C mode. But then I'm still upset about <stdbool.h> it should get off my damn lawn.
Different strokes for different folks, but ... I think stb arrays are beautiful in comparison.
I can never quite get past this one. There seems to be this fantasy that you can write your code without any kind of dependencies whatsoever, and that the user can just drop it into their environment and have it work. Here's what ZeroMQ says about how to install their software:
1. Make sure that libtool, autoconf, automake are installed. 2. Check whether uuid-dev package, uuid/e2fsprogs RPM or equivalent on your system is installed. 3. Unpack the .tar.gz source archive. 4. Run ./configure, followed by make. 5. To install ØMQ system-wide run sudo make install. 6. On Linux, run sudo ldconfig after installing ØMQ.
How is depending on the presence of a massive build system, a set of file system utilities, and a compression utility different from depending on a "3rd party" library that contains an implementation of an RB tree?
The other nits are fair, but this one is not. You mean gzip and tar? Even in whatever hell has banned these tools, it is possible to develop in both C and C++. Downloads just take longer.
The point is the runtime dependencies are small.
It also requires a compiler to build. Where's your rant about that?
(I keep promising myself to stop reading HN threads about C and C++ due to so much inanity. Looks like I caved.)
Is that the point? Because that makes absolutely no sense to me at all in a world of modern linkers, shared libraries, etc.
My assumption is that the OP was concerned about reducing the user's dependency list. And on that score, yes: I could have added a compiler to my list.
The other thing, I think this highlights one difference between C programmers and others, and maybe explains our disagreement. People coding in C often care a lot about the object code. It's not all that uncommon to look at the assembly and question what your compiled has done and make tweaks to get the binary looking better. Hence the disconnect between me saying runtime dependencies are the most important thing and you saying you don't want to install autotools to build. To me the latter complaint comes from a completely different planet.
All's I'm saying is that that seems to me pretty silly. It can't be about reducing the user's burden, because you're going to burden the user anyway with all kinds of toolchain dependencies. It can't be about the runtime, because the user will, except in rare circumstances, hardly notice; if they have autotools, they have glib.
I can think of million reasons not to use C. But, "Well, I don't know. I'd have to use a third party library. . ." Really?
Side note: red-black trees are actually pretty terrible on modern hardware for the same reason linked lists are, b-trees are usually a better alternative.
C++ is not a better C.
This was a really common attitude about a decade ago: don't use C for anything, just point cpp at your C code and use whatever random collection of C++ features you think make your life easier. We've learned since then that that's a recipe for disaster, and you should either write a C program in C (and save yourself the heartache) or embrace the fact that C++ is an entirely different language.
The author is clearly committed to writing a C program and doing that in any language other than C is a mistake.
C++ has its place and its strong point but it is not a strict superset of C in terms of features and improvement.
(There is an Intel compiler that does support C99 btw if anyone is interested but you have to pay for it).
http://software.intel.com/en-us/articles/c99-support-in-inte...
Who says this?
Citing Andrew Koenig 's 1989 statement: "As Close as Possible to C, but no Closer".
Thankfully I would add, as C++ has more strong typing than C.
As for doing pure C99 development on Windows, there are other options as you point out.
Personally as a software developer I don't have issues to pay for the work of others.
> Thankfully I would add, as C++ has more strong typing than C.
Doesn't matter. My project was large, already written in C99, was not in C++. I do not like C++, I don't care for learning virtual destructors mixed with mutliple inheritance, templates stl and friend methods. I am not the only one on the project. So just "don't use it" is easy to say for one person project, when multiple people work on it everyone start to use their favorite subset from C++ huge specification and I don't like that.
> As for doing pure C99 development on Windows, there are other options as you point out.
It was annoying but we just rolled back C99 specific features.
Yes, this is true. For Microsoft C is a legacy language that can well be replaced by C++ for what they care.
But I think I never saw them stating "... C++ is just C and ++ ...", even of the official communication done by Herb Sutter.
I am on the opposite field, I only touch C if really obliged to do so, which does not happen since 2001.
So I am a bit biased regarding Microsoft's decision, but I do understand some developers would rather stay on C land.
The declarations do not look nice (I don't think that is a matter of taste in this case, compared to overloading in C++), but for the principled who want to use C or for those who have to use C, there is a solution.
How so?
a. The article isn't really about C++ in any general sense.
b. The author fails to learn the solutions that are commonly used in C++ to solve his problems so auhtor fails to teach the reader anything
The article is about a couple specific design pitfalls that occur when using C++ exceptions and how the author never solved them. The only conclusion is avoid the design anti-patterns the author used.
So what anti-patterns did he use? The author used exceptions (an optional language feature best used for abort handling of RAII operations) to handle return codes from paths inside a function (which is usually handled with error codes, not exceptions). Not a real problem except that the author failed to carry sufficient context through to the catch location and therefore couldn't clean up (although clean up should have been handled by RAII destructors, catch should only ever need to clean up "this", which should know its own state). Author also failed to catch with sufficient locality and hence made his code confusing.
Instead of realising that exceptions weren't well suited to error code handling and he wasn't throwing enough context information and he should have been using RAII anyway, the author wrote off exceptions for all tasks – including constructors (which is a poor choice since it eliminates all possibility of RAII from the program). This can still work but then the author couldn't work out how to write a factory method that potentially returns an error code instead of an object and instead juggled the ugly state problem of extant but invalid objects. Author could simply have returned a boost::optional<my_class_which_can_fail_construction> from a factory method instead of requiring all that nasty state stuff inside his class.
Instead of learning from many mistakes, author blames entire language.
1. Try to define classes where construction can't fail and the class is always in a valid state. This works most of the time. Failing constructors are pretty rare in my experience.
2. If it can fail, limit it to classes that can only be constructed on the heap. Encapsulate the failure code in a static method that wraps the constructor which can itself never fail. In other words:
class Foo {
public:
// Creates a new Foo or returns NULL on failure.
Foo* create() {
Bar* bar = doThingWhichMayFailAndReturnNull();
if (!bar) return NULL;
return new Foo(bar);
}
private:
Foo(Bar* bar) : bar(bar) {}
Bar* bar;
};
This way it is impossible to get a Foo that's in an invalid state, but no exception-handling is required. The caller does have to check for NULL, of course.3. If I do want to have a class that can be stack-allocated (usually so I can use RAII) and can possibly fail, define an explicit invalid state for the class and check that. Like:
class Connection {
public:
Connection() {
connected = openConnection();
}
~Connection() {
if (connected) closeConnection();
}
// Outside code is responsible for checking this.
bool isConnected() const { return connected; }
private:
bool connected; // True if in valid state.
};
I think I generally have the same philosophy as the author. I don't like exceptions (in C++, I love them in other languages), and I really don't like broken-state objects. But that doesn't seem insurmountable to me.The Google coding guide, at least a few years ago, prohibited throwing C++ exceptions.
So you are hardly alone in avoiding them.
Create a private constructor with a friend function: a "static" factory method. Bam, no more "semi-initialized" crap that you have to deal with.
But since it is entirely contained within the class now, external code doesn't have to deal with the ugly crap that is inside of the class. IE: A perfect separation of concerns. Adding new exceptions to the constructor isn't a problem, because it is a private constructor... only "friends" can access it. There are a _very limited_ number of functions that can use the constructor.
Are there any non-mundane error states that would occur during a stack unwind that you can't safely ignore?
But proper cleanup of non-mundane issues is a well known problem in all programming languages, C included. If you don't handle the mutex correctly 100% of the time, issues come abound. At least in C++, there is a methodology / philosophy (RAII) that handles most situations.
But a lot of the simpler patterns, like static factory methods, are very useful in day-to-day programming.
Decorator is a cool idea, but I haven't found much use for it in practice.
Not sure what "spends no time at HN" means. Martin is an unusually excellent software engineer by almost any measure, as evidenced by his years of impactful open-source development - 0MQ being just one example.
And Haskell wouldn't make sense for a project like this, only because of some of his goals with the project - which include: to eventually have it integrated in the Linux kernel; to make it easily usable by code in any language; to have it easily and universally cross-platform; etc.
(Dang... did I just feed a troll?)
"C" gets you there, if you keep clean "C" interface, and don't let exceptions escape if you have been somehow using C++ internally.
I've had no problem using zeromq directly through the luajit ffi bindings for example.
I'm struggling to understand the business about constructors, though. Shouldn't a constructor be mostly just providing a sane initial state? What's it doing which has the possibility of needing to throw an exception? I think there's a reasonable middle-ground here, where you put the no-fail stuff in the language-provided constructor, and then have separate initialization functions (with return codes) which take care of the more brittle bits as necessary.
What's an example of an object which requires complex initialization? (I'm assuming that anything IO-related, like a socket, will for testing reasons have been passed in to the constructor rather than created by it.)
- external API is C only
- no exceptions
- some macro magic to allow customization of allocators
- a proxy allocator for the STL containers to use my custom allocators.
- only trivial constructors
It's a pleasure to work with. True, the allocators in C++ suck, but things could be worse and I can still use most of the STL without much troubles.
I'm not saying exceptions don't have their gotchas ([1]), however, with all due respect, I don't think that your article does a good job at convincing the reader that using exceptions is a poor choice.
[1] http://ptgmedia.pearsoncmg.com/images/020163371x/supplements...
Return codes in C are easily ignored even if they're serious. In C++ you are compelled to deal with error conditions even if your action is to ignore them. This is a fundamental philosophical difference.
Better title: "Why I should've learned C++ thoroughly before writing ZeroMQ".
There was some bitching about allocators, for example, as if he didn't know about the C++ allocator override feature which isn't even hard to implement. Then there was more confusion about template objects for things like `vector` where he was using them as you might a C linked list library, then complaining that you had to allocate twice as many objects.
I think the author is dimly aware of what C++ really is, and just refuses to play along because they'd rather be writing C code anyway.
John Carmack could probably tear apart every single one of those complaints in ten minutes and have time left over to talk about his new CTO position. That's because Carmack spends the time to learn his tools inside and out and doesn't simply bitch about things being not to his liking.
C++ is a broad church, and there is room for all sorts!
For instance, do you prefer cheap copies? References? Reference counted pointers with auto-destructors? There's many ways of doing it, you're not necessarily forced to pick any particular one, but not making use of these facilities and instead doing it C-style is making your life harder than it should be.
Between the Standard Library and Boost, there's a lot to pick from. There's no reason to go and roll your own vector just because you don't like or understand the way the standard one works.
I guess I mean, shouldn't the stigma be attached to unmaintained or immature libraries not simply third party ones?
The two aren't remotely the same - I find it fairly tedious (but explicit, which is sometimes nice) in C, but reasonably painless in C++ with unique_ptr and similar things.
As has been mentioned in other comments on this article, treating the two as the same language, or versions of it, is a mistake.
[0]: http://nanomsg.org
http://www.freelists.org/post/nanomsg/0MQ-compatibility-libr...
Exceptions:
> The decoupling between raising of the exception and handling it, that makes avoiding failures so easy in C++, makes it virtually impossible to guarantee that the program never runs info undefined behaviour.
The example is basically that when you call a function, it could raise any error, so we don't know which to catch. This problem exists in most languages where errors can occur (I've had to deal with this a lot in Python). So, C. How do you translate the errors across function calls? are you, or are your functions just returning 0/1? (at which point, you've lost much of the richness of exceptions, I think) Instead of handling errors now at the source and handler, you'll also need to handle them at every intermediate point (if (f()) { free things; return ???; }), and this burden makes exceptions wonderful.
> Consider what happens when initialisation of an object can fail. Constructors have no return values, so failure can be reported only by throwing an exception.
Raise an exception, except… (no pun intended)
> However, I've decided not to use exceptions.
(You're fighting the language.) Is this just a complaint that maintaining exception safety in ctors is difficult? unique_ptr and shared_ptr (and RAII) make it fairly straight-forward and easy, and often a ctor can be completely exception safe without try/catch. (If releasing the partially constructed state is the issue, it's hard to tell.)
> if termination can fail, you need two separate functions to handle it
Yes, you will. Honestly, this never sat well with me, but I think it makes good sense. But his example seems to imply you can fold them into one function in C, which you simply can't (under the same assumptions that the C++ code was held to). If "termination" can fail, you'll still need thing_terminate() and thing_deallocate(), or you'll never be able to deallocate the thing; or you're assuming that the C thing_terminate() will just report the error and deallocate anyways. The larger C++ problem remains in C, and the author doesn't touch on it: if you're (manually, in C) unwinding the stack due to error, and you need to release a thing in the process, and that release fails: what do you do? C++ gives you two options, in my opinion: either you ignore it/log it, by not throwing in a dtor, or (and people don't like this one) you throw in the dtor and risk there being two exceptions. C++'s response will be to terminate the program. In C — or any language — what would you do with two errors?
He already started working on "C" re-implementation of ZeroMQ with different API and zeromq wrapper on top of it (so it could be used as zeromq replacement).
If you think your options are only "returning 0/1", you're not being creative enough.
For example COM has a 32-bit integer called HRESULT that all methods return. The high bit says something is an error. A few bits in the high 16 bits indicate where an error is from. The low 16-bits are private to whatever space is specified in the aforementioned bits. So if you take an error like 0x80070002 you can say it's an error, from the Win32 subsystem, and the error code is 0002 or file not found in this specific instance. Another component can set the high bits differently and end up with their own 16-bit error codes. I find when I write COM code the errors are much more meaningful than in a high-level language because it's all very explicit, you're constantly thinking about it at every function call, whereas in exception-based systems it's all just very haphazard and hand-wavy.
Or you could look at something like glib. Each method might have an out parameter which is a gerror object. This includes an error code and a string.
In other words there are several conventions out there. If you pick one and stick with it across a code base you can be very productive with a minimal amount of nonproductive "philosophical questions" like the ones in your post. Even if you have several libraries which have different conventions, you can adapt their conventions to the conventions of your code base and it's typically not a big deal.
> Instead of handling errors now at the source and handler, you'll also need to handle them at every intermediate point (if (f()) { free things; return ???; }),
Actually this "early return" pattern is quite bad in plain C, because if you have N allocations for example, each "return" statement needs to free between 0 and N buffers before leaving the function. That gets very maddening. One way to avoid this is to have only one "return" statement at the end of your function and free any loose buffers if-and-only-if they are non-null. This involves either constantly re-checking some error status, or using a goto to jump to the end of your function when something fails. IMO the 2nd is much cleaner. (Let that wrap around your high-level-language brain... but also know that any large C only code base is doing the same thing if they are sane; grep the Linux kernel for "goto" and you might start to get a picture... and while you're bashing C error handling and probably thinking ill for me for pulling out "goto", please keep in mind your "exception" concept is really just a goto that crosses stack frames...) In the end this pretty well simulates what you'd do in destructors in a C++ code base following the RAII pattern.
> and this burden makes exceptions wonderful.
In my experience it, on the contrary the exception concept just makes sure you have no idea that stuff can fail, and every small failure in a "shockingly" unexpected place blows up your entire program and terminates the process. Or you have Java's goofy "checked exceptions" concept and so lazy programmers will introduce do-nothing catch blocks to shut up the compiler.
If you want to have an exception that just contains an error code you can do that (e.g. java's SQLException works that way). Personally I find it much more useful to be able to give errors textual name, and declare a hierarchy relationship between errors.
> Actually this "early return" pattern is quite bad in plain C, because if you have N allocations for example, each "return" statement needs to free between 0 and N buffers before leaving the function. That gets very maddening. One way to avoid this is to have only one "return" statement at the end of your function and free any loose buffers if-and-only-if they are non-null. This involves either constantly re-checking some error status, or using a goto to jump to the end of your function when something fails. IMO the 2nd is much cleaner. (Let that wrap around your high-level-language brain... but also know that any large C only code base is doing the same thing if they are sane; grep the Linux kernel for "goto" and you might start to get a picture... and while you're bashing C error handling and probably thinking ill for me for pulling out "goto", please keep in mind your "exception" concept is really just a goto that crosses stack frames...) In the end this pretty well simulates what you'd do in destructors in a C++ code base following the RAII pattern.
Sure - exceptions exist to replace something you would otherwise have to use goto to do, that's pretty much the entire point. But they're better than goto for the exact same reason that a while loop is better than implementing a loop with goto.
> In my experience it, on the contrary the exception concept just makes sure you have no idea that stuff can fail, and every small failure in a "shockingly" unexpected place blows up your entire program and terminates the process. Or you have Java's goofy "checked exceptions" concept and so lazy programmers will introduce do-nothing catch blocks to shut up the compiler.
You have the exact same problem in C. Either programmers have no idea that library functions can fail and never check return codes, or you have something like gcc's warn_unused_result and lazy programmers introduce do-nothing error-code checks.
You forgot the other option. The careful programmer knows they have to check return codes and is suspicious when it's not happening. With exceptions you have no idea when they can happen because it's not part of the function signature. Or in the case of Java, handling errors is like homework, a constant struggle to keep the nagging compiler happy, instead of a consistent style that you apply throughout a code base.