It Would Have Been Possible
The feature was implemented in other languages of the time. I’m not aware of a specific quote by Dennis Ritchie or Ken Thompson explaining their reasons for not including it. However,
There Would Have Been Trade-Offs
Consider the simple example:
int bar(y, z)
/* Do stuff.*/
/* Do stuff. */
/* Do stuff with x, foo() and bar(). */
There are a few different ways the language could have handled this:
Nested Functions That Don’t Capture Their Environment
This is similar to Rust (which has separate closures). If Dennis Ritchie had gone this route, the low-level implementation of nested functions could have been exactly the same as top-level functions. Neither
foo::baz() would be able to refer to the
x parameter of
foo() (unless the programmer explicitly copied and passed it to the inner function). Here,
foo() could return a pointer to either function, and any other external code could safely call it.
On the other hand, there would be nothing you could do with these nested functions that you could not accomplish with C as it exists, by writing
static int foo_bar() as a non-nested function. The behavior is the same, and all you are adding is the ability to place the function definition inside function scope. Nesting the function might make it easier for an optimizer to see that there are only one or two paths of control flow that could possibly reach some code.
C Might Have Had First-Class Closures
C++ ended up adding several different variations on this idea in the twenty-first century: lambdas,
std::function objects, member-function pointers,
std::bind, and I think even a few more.
This would be more complex than function calls in C, but one fairly-simple way to enable it is to pass the daughter function a pointer to the parent call’s stack frame, and the grandparent’s if the function is doubly-nested. This would allow a nested function to access variables in the scope of the most recent call to the functions it’s nested in, even if there are arbitrary recursive or sibling calls in between.
One complication here is what happens when you take a pointer to a nested function. There are legitimate use cases for this, but can such a function pointer be returned from a function, passed back in as an argument to the same function (perhaps through a pointer-to-function-pointer input-output parameter), and called on a different invocation? Would such a call capture the original environment, or the current one? Can a returned pointer to a nested function even be called from outside the parent function?
Functions in early C are very weakly-typed, and the type of a nested-function pointer would have been a leaky abstraction. For example, if the syntax had the type “doubly-nested function returning
int,” and allowed one to be called with no other information about the context, all implementations would have to support that.
It Might Have Been Restricted for Simplicity
The complexity of the previous section goes away if it’s illegal to take the address of a nested function at all, and most of it goes away if the closure is considered a temporary whose address cannot be returned from a function. Classic Pascal, which had a lot of influence on C, did not have function pointers. The designers might also have introduced stronger function typing much earlier than they did.
C++ Ended up Doing it a Different Way
In the example I gave above, though, there’s no compelling reason to use closures.
foo(x) has only one
int argument, and it would be simpler and faster in this example just to pass that to
baz(x,w) if either actually needs it. This would only be more efficient if there were a large number of parameters to be captured.
It’s very unusual to write functions with many arguments, though—especially in K&R, where the compiler cannot even type-check them. Typically, when it grows big enough to be inconvenient, the parameter list gets refactored into a
struct. Once you do that, the local state you’d be encapsulating in a closure would all be in that
struct, so you can just use the
struct to roll your own closure. Maybe pass it in a pointer, and call it
this. Then, to be able to call it from elsewhere in the program, you would need a dynamic function pointer—or even a dynamic table of multiple function internal functions, such as
baz() are both part of the same scope and can call each other. So maybe you put a virtual function table as part of your
this structure. Congratulations, you’ve reinvented the
class. C with Classes, which evolved into C++, began just as syntactic sugar for that. And first-class closures would have been different syntactic sugar for something very similar.
This is a very popular paradigm, but the C language committee decided that it would be better to keep the language small and simple. And closures would have mostly just duplicated the same functionality.