Hacker News new | ask | show | jobs
by dandotway 1610 days ago
Features that seem like a good idea at the time often don't stand the test of time 20-30 years in the future. In the mid-90s Object-Oriented Programming was super-hyped so a bunch of other languages bolted on OO, such as Fortran and Ada. But now we have Go/Rust/Zig rejecting brittle OO taxonomies because you always end up having a DuckBilledPlatypus that "is a" Mammal and "is a" EggLayer.

A great strength of C is that if you want more features you just go to a subset of C++, no need to add them to C. C++ is the big, ambitious, kitchen-sink language. When C++ exists we don't need to bloat C.

Fortran was originally carefully designed so that people who aren't compiler experts can generate very fast (and easily parallelized) code working with arrays the intuitive and obvious way. But later Fortran added OO and pointers making it much harder to auto-parallelize and avoid aliasing slowdown. Now that GPUs are rising it turns out that the original Fortran model of everything-is-array-or-scalar works really well for automatically offloading to the GPU. GPUs don't like method-lookup tables, nor do they like lambdas which are equivalent to stateful Objects with a single Apply method.

Scientists are moving to CUDA now, which on the GPU side deletes all these features that Fortran was bloated with. Now nVidia offers proprietary CUDA Fortran which is much more in the spirit of original Fortran, deleting OO and pointers for code that runs on GPU. If the ISO standards committee didn't ruin ISO Fortran for scientific computing by bloating it with trendy features we could all be running ISO Fortran automatically on CPUs and GPUs with identical code (or just a few pragmas) and not be locked in to proprietary nVidia CUDA.

But GPUs are now mainly used for crypto greed instead of science for finding cancer cures or making more aerodynamic aircraft so maybe it all doesn't matter anyway.

4 comments

Yeah. I think I'm much less informed on this topic, but my initial thought on reading the "Rationale" section was that this sort of feature would only be helpful in cases where C offered almost no advantages over C++.
> A great strength of C is that if you want more features you just go to a subset of C++, no need to add them to C. C++ is the big, ambitious, kitchen-sink language. When C++ exists we don't need to bloat C.

This is a rationalization, and a bad one. When your solution is "just pull in another programming language", you have a problem.

"Another programming language" cannot even meaningfully exist if all programming languages are forced to have the same feature set. Should Python get C-like low-level pointer manipulation so that Python users don't need to "pull in another programming language" of C to do pointer manipulation?

C doesn't need "defer" because C programmers have managed since the 1970s to implement operating systems, compilers, interpreters, editors, etc., just fine without it. Those who want a bigger C can use C++, this pond is big enough for two fish.

> all programming languages are forced to have the same feature set

Good straw man there. Did I say all languages need to be exactly the same? This comment just looks like something you can fall back on to reject any feature addition to C. Its too bad really, as its sentiment like this that is killing the language. Many people are sick and tired of old, crusty C, where it takes close to a decade to add or change anything. I like the idea of a small, performant language, but when you put such a stranglehold on changes, you choke out most chances of innovation.

> I like the idea of a small, performant language

So the earliest C compilers were under 5000 lines of C+asm:

  https://github.com/mortdeus/legacy-cc
If you want a minimal "standard committee approved" C89 compiler then David Hanson's lcc and Fabrice Bellard's tcc both come out to over 30,000 lines. To understand C89 fully you at a minimum have to read a ~220 page (14,248 line) copy of the (draft) ANSI standard:

  http://port70.net/~nsz/c/c89/c89-draft.txt
I don't know what the smallest C23 compiler would be with all the new features since C89 added, but it's at the point where a single human can't implement a C compiler anymore. It's becoming a language only rich corporations have the wealth and power to implement and steer.
On the other hand, some features turn out to be a very good idea and do stand the test of time. Designated initializers and compound literals, introduced in C99, are perfect examples of C features that stuck and became very widespread, while keeping the spirit of the language. C shouldn't be set in stone.

The fact that goto-based solutions and a non-standard GCC extension are common methods of resource cleanup in C today seems to suggest that a standardized language construct for resource cleanup would be appreciated.

> A great strength of C is that if you want more features you just go to a subset of C++, no need to add them to C.

What is C for then? Cleanup of function-scoped resources is a major concern in every large C codebase I've seen.

It's not an major concern though.

If one has trouble writing correct cleanup code conventionally (with "goto out" and a single function exit), then allowing them to use defer will only lead to more obscure issues.

And if defer is meant to make code slimmer, it still doesn't belong to C, because it leads to implicit execution and memory/stack allocation.

C is an explicit and verbose language. What you see is what you get. This is the spirit of the language. Unlike with, say, C++ where "a + b" may actually produce kilobytes of machine code, because + just happend to be overloaded.

> If one has trouble writing correct cleanup code conventionally (with "goto out" and a single function exit), then allowing them to use defer will only lead to more obscure issues.

I've written countless functions in this style and I don't enjoy it. I think it's better than the other styles of resource cleanup in C, but it's not ideal. In this style, whenever I add a resource to a function, I have to go to the top, add the declaration (with a sentinel value,) then go to the out label, check for the sentinel value and conditionally destroy it. I'd much rather add the declaration, initialization and destruction of the resource all in one place. That would make it much harder to forget the destruction, for one thing.

> And if defer is meant to make code slimmer, it still doesn't belong to C, because it leads to implicit execution and memory/stack allocation.

I don't get the implicit execution thing, and I don't see how it's like that C++ example. The only code that executes is written in the function itself, inside the defer block.

> I've written countless functions in this style and I don't enjoy it. I think it's better than the other styles of resource cleanup in C, but it's not ideal.

I've got almost a couple decades of C behind me, and I agree. The way we handle cleanup at present is not particularly difficult, but it feels irritating. I'd imagine most C programmers agree that the goto based cleanup handlers just happen to be the best we've got, and aren't necessarily ideal.

> And if defer is meant to make code slimmer, it still doesn't belong to C, because it leads to implicit execution and memory/stack allocation.

I don't see why block scoped defer should cause any more memory or stack allocation than a goto based cleanup handler. It's just a different way to organize the source code. In some instances it might actually allow you to omit some local variables (that otherwise would have to be optimized out by the compiler).

> C is an explicit and verbose language.

It's relatively explicit, I agree. However, defer doesn't change that much. You still see exactly what code runs inside your function. The only real change is that code's location. It's not that different from putting expression-3 in your loop header and having it be evaluated implicitly when you reach the end of the body or do a continue. If you wanted to be explicit, you'd ban for loops and use gotos in a while loop to replace continue. Umm, be my guest, but I prefer the less verbose approach.

And that gets me to the second point... C can be surprisingly terse despite requiring you to be rather explicit, and that's one of the things I really like about C. If anything I'd love to see features that allow it to be even more terse.

> Unlike with, say, C++ where "a + b" may actually produce kilobytes of machine code

Oh, I agree. I really don't want tons of hidden code in C. However, the deferred block is still explicitly coded inside your function and not at all hidden from you someplace else. So it's not like you need to go spelunking through a pile of headers and class definitions to discover that there are destructors running SQL queries when your function returns.

In that respect, defer remains very explicit and transparent so I'm ok with it.

> block scoped defer

That's the thing. Block-scoped is a better option as far as the language "spirit" is concerned, but it's limiting (see below). Function-scoped is more useful, but when used in loops it may lead to unbound stack usage and that sorta goes against the rest of C, because no other _language construct_ comes with such lovely side effect.

Re: limiting - It's not uncommon for a function to need to grab some resource conditionally and then use it in the rest of the function code, e.g.

    void foo()
    {
        bar * b = NULL;
        if (x && y)
        {
            this();
            b = that();
        }
        ...
        baz(1, 2, b); // b may be null
        ...
        release(b);
    }
This can't be handled with block-scope defers. This needs function-scoped ones.

A better option would (probably) be to allow binding defers to a specific on-stack variable... but that's basically a destructor and that opens its own can of worms, not all of which as technical.

It seems a bit limiting, yes, but this does not seem like a major limitation to me. Especially if we compare it to how existing practice with goto based cleanup handlers would work in this example. It doesn't really matter that the resource was obtained in a block, the variable holding a reference is still scoped to the function body and will be checked at the end just as it would be with goto.

    void foo()
    {
        bar * b = NULL;
        defer [&]{if (b) release(b);}

        if (x && y)
        {
            this();
            b = that();
        }

        if (something_gone_wrong())
        {
            return; // no problem, b gets released if it was acquired
        }
        ...
        baz(1, 2, b); // b may be null
    }
If making the release conditional seems a bit hacky, remember that you need that sort of thing anyway for the hugely common case where you allocate & initialize a bunch of things and then let the caller keep the resources, except if there's an error.. in which case you need to clean everything up. Without some additional language features (first class error types or "error returns", then error defers?) these conditions are unavoidable.
Sticking defer under the var declaration is clever, but it doesn't look an improvement in terms of the code quality to me. It trades verbosity of the "out:" pattern for the need to register the cleanup code before the acquisition code. That's just weird. It's not complicated, just... backwards. Almost like a solution in search of a problem :)
> GPUs don't like method-lookup tables, nor do they like lambdas which are equivalent to stateful Objects with a single Apply method.

Since I tend towards read-only instance data, I often live my life considering an object to mostly be a bag of closures with a shared outer scope.