Skip to the content.

Another look back at C

I just worked on C code again for the first time in a long while. I’ve worked almost exclusively in some kind of managed runtime environment – Java, C#, Python, or Javascript in a browser. So working on C again was like going through a bit of a time warp. I spent a while trying to figure out how to connect the old stuff I used to know with the new. Like, are gcc, gdb etc. all still there? And how do I get them to work with VS.Code, and on MacOS?

Once the logistics were out of the way, I had some observations about the differences between working in C and working in a managed environment, and how I had missed some of the bigger themes when I first learned Java several decades back.

Garbage collection is more than a convenience; it changes your programming patterns for the better. When I switched from C/C++ to Java, my younger and more naive self thought, great, now I don’t have to worry about where to free things that I malloc‘ed somewhere else. The deeper point, which I didn’t fully appreciate at the time, is the extent to which garbage collection is a prerequisite for functional programming patterns like pure functions, filter/map/reduce etc.

Most of the C code I was working with, had subroutines that would “return” results via mutating blocks of memory allocated and passed in by the caller, rather than by constructing and returning new objects. Often these are arrays allocated on the caller’s stack, which avoids concerns about whose responsibility it is to free memory and when.

When you can allocate new arrays or objects without worrying about who has to free them, though, you can write pure functions that take immutable arguments and return new objects. Aside from the benefit to readability that comes from returning things by, well, returning them, you get additional benefits of function composition and thread safety. Without garbage collection, you can’t write a pure function, because even returning a simple string requires your function to allocate memory on the heap and the caller has to worry about when to free it.

Exceptions are a bigger deal than replacing magic flag codes with more meaningful names. Without exceptions, code ends up littered with boilerplate conditional checks. You have to explicitly check every possible null-pointer or index-out-of-bounds condition explicitly to avoid the whole program from crashing. With exceptions, you can add a catch somewhere and a runtime exception from ten layers deep will percolate back to your catch, giving you a chance to log and recover but not crash the whole program. Without exceptions you’d have to deal with each one of those levels.

This is also a complaint I have about Go. Go has a unique system of error types that solves for separating errors from normal return values. But Go does not have a good answer for exception propagation. Exceptions have to be propagated explicitly with lots of if err != nil { return nil, err } boilerplate.

Written on November 25, 2020