Not the parent commenter, but namespaces alone make the switch a no-brainer. Add on top RAII, actual abstractibility (good luck implementing std::string in C properly) make it a strictly worse language.
Why ado you think namespaces make it a no brainer? I would argue the only thing I would want from C++ it's operator overloading, and I'm not even conviced about that.
I hope it's not mandatory. I'd much prefer static analysis to tell me of use-before-acquire errors: I have a number of hot-inner-loops where initialization nukes performance.
In the context I'm thinking of, I have fairly sizable arrays -- up to 512 bytes; usually, only the first few bytes are used (for state tracking); an `int` tells me the high-water mark so the data is only read after being written. The amount of work int the loop is (in almost all cases) only 5 or 6 instructions before termination. Initializing 512 bytes is best case 8 ops, which is >100% overhead. The code is recursive, but bounded to a depth of 8, wity internal linkage (only two `int`s and a pointer are passed) in the tail call position, so the calling convention is just three registers. Even a compiler like q9x, or clang with no opts, produces excellent code. But, even a sniff of initialization tanks perf to the tune of 3x.
RAII in C++ does not mean that all bytes are initialized, only the ones you want. In
struct Foo {
int header = 0;
float foo;
int data[64];
int footer;
// just to show that having an explicit constructor / dtor does not change anything
Foo(): footer{456} { }
~Foo() { }
};
It's not a matter of optimization, absolutely no compiler will zero-initialize unless you explicitely ask for it (or you are in a situation where the language mandates initialization, like global static in C and C++)
Well don't know about the parent but you ruined my day.
Thing I keep coming back to is the proportion of effort you have to spend learning and keeping up with your tools vs the problem domain your working on. Feels like a big problem with C++ is how much energy the language itself uses up.
A friend of mine is, unlike me, really really smart. He likes golang and hates C++. And not like he hasn't spent years professionally writing C++ code. I think the advantage of golang is he can write it reflexively. So all of his attention is on the problem not the language.
Me I feel like the problem with C is not so much the language. It's that I'm always worried about stepping off a ledge. Things like defer would help. Because failing to clean up resources is a big problem with C.
But is failing to clean up transient resources that are only allocated for the duration of one function (or block) really a big problem with C? These tend to be the easiest ones to handle (but admittedly a bit annoying & verbose without defer) and spot if missing. (Also: static analyzers are relatively good at pointing out resources leaked in a single function.)
I think most of my functions that allocate a resource only release it on error; otherwise the allocated resource lives on and gets freed later by another function. Defer doesn't help at all here. It just helps with the trivial ones..
The silver lining is that really 99% of the time it just doesn't matter. I just explicitly initialize everything to zero/nullptr (unless I have a specific reason to want a different explicit value), and I only worry about looking up the particular rules if I need them in a situation where I know initialization can be a significant cost like a gigantic array. It is annoying to look up every time that happens but it's not that often.
This link is exactly what I'm saying, if you don't ask for it you don't get initialization. There are multiple ways to ask for it because the person who writes the type may eitherwant to take on the responsibility of initializing entirely by initializing on the ctor, or delegate it to the call site which can then either initialise (T t{};) or not (T t;).
The SO post looks confused ; A, B, D, E and F are the exact same case wrt the initialization of the int. It does not matter that there's a constructor or not and which shape it has, only that the variables get initialization somewhere or not (and that can be in the ctor's member init list, in the struct définition or even when creating a value if aggregate-initializing).
I'm sure you know what you're talking about wrt C++ spec, but you ignored the main point. It's easy to be confused by stuff like that. It's hard to read C++ code and know (other than intuit) what it does. For a lot of people including myself, at least.
Prototypes were in the original C standard of 1985, before first edition of The C++ Programming Language was released and long before C++ was formalized standardized.
While the first draft was released in 1983, the first ratified C standard was in 1989.
The first edition of "The C Programming Language" (from which I first learned the language) was published in 1978, over a decade before the language was standardized.
You are correct that C89 added function prototypes. However, contrary to your implication, C borrowed the concept and syntax from C++.
Everything I said was correct. I was a member of X3J11 (which was established in summer 1983), the C language standard, and was the first person on the planet to vote to approve it (due to alphabetical order) ... that was in 1985. It was several years before it was ratified, the delay having to do with standardization politics which required a lot of buy-in. I have no idea why you're mentioning the first edition of K&R from years earlier, which of course did not have prototypes.
There's a notable difference between C's prototypes and C++'s ... `int foo();` in C is not a prototype, whereas in C++ it's equivalent to `int foo(void);` ... C had to maintain compatibility with K&R style declarations, whereas C++ didn't.
..Except that in C that'll turn into stack allocations and will easily segfault due to a stack overflow. So you can't actually use it like that in practice (unless you can guarantee your loop will be called a small number of times). You probably even won't notice while writing the code, and will just get random segfaults wherever you have a defer in a loop when you hit a large enough iteration count. Never mind it being very inefficient while it doesn't.
Well, we are earnestly analyzing a silly little example program, but okay :). The equivalent C code wouldn't produce stack-allocated mutexes unless that's what the programmer wanted. E.g. the POSIX pthread functions don't care where your mutexes are allocated, since they are always passed by reference.
It's not the mutexes that'd be stack-allocated, but the list of things to call back to at the end of the function. The locks list could be modified or freed by the end of the function, but something still must hold the list of things to deferred-unlock.
The pthread_cleanup_push/pthread_cleanup_pop thing presumably keeps its own heap-allocated vector, backed by malloc or something. C itself can't willy nilly heap-allocate, so that list will be on the stack. But the stack is tiny compared to how long loops can be. Hence stack overflow.
C libraries -- including the runtime implementations of features like this -- can heap allocate just fine. They just need to return pointers on the stack to the heap allocated values, either directly or indirectly (e.g. buried in a struct return value). As is always the case with C, the burden to free the memory is on the caller: no problem.
Given a possible implementation, this loopy mutex example could have a tiny stack footprint: a single pointer to the shared cleanup() function; and a single pointer to the head of a (heap allocated) linked list of pointers to mutex (i.e., the function arguments). And the function pointer would not necessarily require allocation at all, as we can statically point at the function definition here. So we are down to a single word of stack allocation.
Who's going to construct the linked list, and where does it live? That's what the parent comment is pointing out.
In the general case I see no alternative to either the compiler generating one alloca() per defer or heap allocating defer callbacks. Both are terrible solutions for C, because alloca can overflow, while heap allocations can fail with an error code and defer has no way to catch that error. Besides, C programmers just won't use the feature if it requires allocation out of performance concerns. Block-scoped defer is the only reasonable semantics.
Same question in return: who's going to alloca() or heap-allocate the defer callbacks? How is that substantively different from maintaining a linked list? As soon as compiler support is on the table -- i.e. we're not limited to using some too-clever cpp macrology and a support library -- then virtually any implementation is possible. There's obviously more than one way to do it.
> C programmers just won't use the feature if it requires allocation out of performance concerns.
I agree that many C programmers wouldn't touch the feature for performance reasons. But let's not pretend that every C program is a video driver, a AAA game or a web engine. Many, many large C programs would benefit immensely from `defer` semantics -- otherwise, why would the GCC feature exist -- and they are performance-tolerant enough that a little heap allocation would be a reasonable tradeoff for increased safety.
But I'm not really defending `defer` in the first place...
> Block-scoped defer is the only reasonable semantics.
I agree with you completely. :) I was never defending function-scoped `defer`, but answering claims about the necessity of stack allocation. There are possible defer implementations that wouldn't blow the stack: that's my only point.
It's healthy for culture-conscious programmers to reflect, now and then, on just how small a segment they represent. Cultured programming is fine, but it's like opera: the ordinary programmer recognizes a few of the tunes, but they don't sing along. It's hard to appreciate just how much uncivilized business code is out there, when nobody is getting HN likes for keeping that 1980's ERP running.
> Besides, C programmers just won't use the feature if it requires allocation out of performance concerns.
Nevermind embedded platforms where heap might be unavailable or just so scarce that its use beyond early initialization is strictly verboten. Or interrupt handlers where you simply can't call an allocator.. Block scoped defer could still be useful on such systems (e.g. with locks).
Of course. And if you were writing your embedded system in C++, you'd avoid EH and other non-zero-cost features. That doesn't mean that these features aren't useful in other contexts. I submit that the world of C and C++ programming is vastly larger and more diverse than the world of interrupt handlers and embedded systems.
...But as I pointed out in a sibling comment, I was never defending function-scoped defer. I agree with you. I was pointing out that the implementation of such a feature wouldn't require excessive stack allocation.
defer, as proposed here, isn't a library feature though, it's a part of the core language. I'd like to be able to use it, but if it can ever do a malloc (which is horrifically slow compared to not having one), it's just infeasible.
But I looked through the spec again, and it actually just says that defer outside the top level is implementation-defined, so this is irrelevant anyway.
I admit that I didn't read the actual article. :) I was using "library" in a broad sense, just to mean the runtime code that you didn't have to write yourself.
C doesn't exactly have "runtime code that you didn't have to write yourself" though. There's libc, but you can easily disable it, and having any form of defer be unavailable then is just bad. Everything else comes from your code, the headers it includes (which don't contain function definitions), and statically linked things.
Sure. And just like libc, you could easily disable (by not using) a theoretical libc_defer that provided defer semantics. Kind of like libm, you use it when you need it.
This has been a fun conversation, and I really enjoyed chatting with you about this. :) Take care.
That code is odd (though I assume idiomatic Go). I'd much rather have block scoping with something like this:
for _, lock := range locks {
lock.Lock()
// Do something with lock
}
And I don't really do Go, but in Rust, if I wanted to lock some arbitrary list of locks for a whole function, it would just be something like this at the top:
let _ = locks.iter().map(|l| l.lock().unwrap()).collect::<Vec<_>>();
> To acquire locks in a loop and release them at the end of a function.
that looks like a super big gotcha, hasn't there been enough bugs with alloca() being called in a loop yet to show how risky and unintuitive this is ? block-scoped is explicit and explicit is good
I can see one use that when using lock ordering to prevent deadlocks (http://tutorials.jenkov.com/java-concurrency/deadlock-preven...), but then, the locks to be taken are a fixed set, and one probably would have function take_locks(lock *) and release_locks(lock *), and one could do defer release_locks.
“In most systems, this information is unavailable, making it impossible to implement the Banker's algorithm. Also, it is unrealistic to assume that the number of processes is static since in most systems the number of processes varies dynamically”
I see this code and all I can think is that you’re going to be spending the rest of your life debugging threadsafety issues. I can contrive a case where this is useful, but where in the real world?
The semantics of block-scope defer are much simpler, easier to understand, and faster. The compiler always statically knows which defers execute at any given point, which aids optimization. With function-scope defer, the semantics are extremely dynamic, requiring bookkeeping of a runtime stack of defer thunks, and compilers have a hard time optimizing it.
Function-scope defer is something that surprises everyone I explain it to. Programmers naturally expect defer to be block-scoped.
>With function-scope defer, the semantics are extremely dynamic, requiring bookkeeping of a runtime stack of defer thunks, and compilers have a hard time optimizing it.
As far as I know, most functions have only one defer, and Go optimizes such cases quite trivially, by inlining calls to the deferred function at every function exit at compile time, without managing an additional stack. If there are several defers, then yes, the slow path is used.
What in your view makes defer a bad feature of Go? Maybe my bar is low, but each time I jump back to C I wish I had defer and end up abusing __attribute__((cleaup)) instead.
The C you will get to learn is also planning to have lambdas, and they are being based on C++ design rather than Apple's blocks, so the example assumes they will also be in C2X.
>Also why is RAII bad? It's an awesome feature in C++
I didn't mean it's bad (I used to be a C++ developer myself and enjoyed RAII a lot), just wondering what are the alternatives that the OP doesn't consider "hacks". RAII would require to introduce constructors/destructors in the language, with all the gotchas (and probably you'll want a full-fledged OOP after that), which is apparently against Go's design principles as a simple language.
>Auto-Closable interface with syntactic support.
I don't see much difference here in practice; the whole difference is that in C#, for example, you use "using" on a whole object, while in Go it's a "defer" on a specific method of the object (or a standalone function). You are not limited to a single method and can use it on any method you deem necessary.
Auto-closeable/RAII, however, is less flexible in ad hoc situations specific to a certain function (you have to define dummy classes just to make sure a function is called no matter what), Go allows to use "defer" on a lambda. Auto-closeable also ties control flow to an object, which makes sense in an OOP-focused language, but Go isn't one.
I agree with most of your points, but I wanted to also point out that you don't need C++'s ctor/dtor spaghetti to have useful RAII: Rust achieves it without even having first-class constructors (and opt-in custom destructors via Drop).
RAII is fine. If it's a language feature, APIs can provide that to you so you don't have to write RAII wrappers all the time.
Otherwise a generic _bracket_ operator could be introduced, similar to Python's `with` statement. In C it might be a bit hard to parameterize, but even if it just requires you to use a void, it's still an improvement vs. not having anything at all. (Talking about language features so `__attribute__((__cleanup__))` doesn't count.)
It’s already exists in other languages and it exists because a function could have multiple different exit points. Even if Go had different error handling there might well be different exit points. Hence why defer isn’t a Go-specific feature.
Defer CAN help with error handling, but you need other language features for that. You need well defined error types or exceptions in some way or another.
Zig has errdefer which only runs if an error occurred below the statement within that scope. It allows you to always keep cleanup locally, but you can still handle errors for your business logic somewhere else.
errdefer is cool, but I don't think C should support it. Mainly because you'd need to spec errors at a language level, and doing that today is probably impossible.
The new defer proposal has the potential to make code easier to read, remove some uses of goto's and help to fix some resource leaks that are so common on old non-GC languages. Certainly a feature I'm rooting for.
Ugh please don't do this. Just standardize the existing __attribute__((cleanup)) mechanism which is already implemented by compilers and widely used in many free software projects.
“this feature cannot be easily lifted into C23 as a standard attribute, because the cleanup feature clearly changes the semantics of a program and can thus not be ignored.”
That's a very weak objection. In any case since most programs are hiding __attribute__((cleanup)) in a macro (eg. glib's g_autoptr macro or systemd's family of _cleanup_* macros) you could use another kind of annotation.
The point here is that the C working group should be standardizing existing practice and helping the existing users of C, and not striking out making bizarre new syntax choices and keywords which are completely unproven.
__attribute__((cleanup)) is ugly because for some reason, the gcc folks decided that the parameter passed to cleanup needed to be a function pointer of signature void(void**) instead of the much more sane void(void*), with no way to allow implicit casting of the parameter to a function pointer of different type, so none of the basic cleanup functions (e.g. free, fclose) work with it out of the box - you need to pollute your codebase with one wrapper for each cleanup function that you want to use.
They should have made it so that cleanup took an expression block:
What are you going to do about it? I suppose you could delete all the C software from your system. Or make a list of all the C programs that your system depends on, and persuade each of their maintainers to change languages.
Or you could just put up with C, like everybody else does, and be quietly thankful that the myriad layers of our modern, complex systems are being maintained by other people.
Sure - I agree! But, it exists and it's widely used already. The C working group should concentrate on standardizing existing practice and not start striking out on unproven and weird new syntaxes and keywords.
The "good" way to go about that would be to have a second "cleanup_value" attribute.
(Or, since the standard would be creating new names, have "cleanup" and "cleanup_ptr" instead. Assuming that this is a separate attribute namespace, which I believe it is[?])
FWIW, you do sometimes need the address of the variable. Particularly if it's some struct that is made a member of some container (e.g. linked list) temporarily - you need the original variable's address to unlink it.
I really don't like it. The lambda stuff, especially the captures that were ripped from C++, don't fit C at all. The question about whether it should be cleaned up at end-of-scope or end-of-function is too ambiguous and debated. And if I'm understanding this correctly, this is the worst part:
> This indicates that existing mechanism in compilers may have difficulties with a block model. So we only require it to be implemented for function bodies and make it implementation-defined if it is also offered for internal blocks.
Since variables can be scoped to blocks, and allocation scopes can be arbitrary, needing neither a function or block scope to bracket them, I don't see how that will fly either.
I think that idea is pretty cool. I don't like the new closure pointer type, and how all functions that take function pointers need to be retrofitted to take closures as well, but I guess it's necessary. I assume closure pointers would be implemented as a double pointer, one void* to point to the captured variables in memory, and one function pointer to point to the procedure. One of the most annoying parts of C is how callback logic needs to be defined completely separately from the calling logic.
I usually use the single-exit-point idiom with gotos to handle cleanup. I now wonder if there is a compiler that helps you enforce that so you don’t randomly stick a return in the middle of your function.
Anyway, this would be great to have in C as it would simplify how resources are handled.
You can use the non-standard, but typically implemented, cleanup attribute today for function-scoped cleanup. I don’t know if compilers inline this kind of “destructor”. It works well — I’ve seen it used successfully at multiple employers.
Not only inline it, but also optimize it away (eg. _cleanup_free char *ptr = NULL; the cleanup path just disappears in places before ptr is assigned to a non-NULL value).
If the purpose of defer is to replace the `goto single_exit`, I think the value captures are unnecessary. Any single_exit I've implemented uses the value at the time of function exit, so that I can realloc in the middle of the function.
It would be a shame if this defer was limited to function scope. It would be very useful in nested blocks as well. But, I would still appreciate it.
defer and auto are the only things I would love to see in C.
I don't mind a sane defer in C, that is something which follows scope in the same way stack allocated destructors are invoked. This follows the behaviour in other languages like Swift.
Why "on function exit" style defers - already known to be a bad idea from Go - is beyond me. Such a solution more or less requires storage of potentially unbounded allocations.
Foo *f;
for (int i = 0; i < very_big_value; i++) {
f = get_foo(i);
// BOOM
defer return_foo(f);
}
I would sort of understand if this was proposed for a high level language garbage collection where actual memory usage is secondary.
In this case you wrap the loop in an anonymous function if you need it to be cleaned up within the scope of the loop.
Or move the functionality to another function.
Adding a statement that would require a dynamic allocation for every iteration of a loop is kind of insane for C.
It doesn't matter what the defer does, if it's allocated in a for statement and doesn't go off until the surrounding function exits, then it's going to have to stuff tons of function pointers and parameters somewhere, presumably alloca'd onto the stack over and over.
That's not C. That shouldn't be C.
There are a dozen languages for doing clever dynamic magical things in code. C is still fairly straightforward. Use one of the other languages instead of bagging down C with complexity until it turns into another difficult C++ variant over time.
> Why "on function exit" style defers - already known to be a bad idea from Go - is beyond me
Is there something you can point me to about this? I write Go professionally and from a readability and utility standpoint I really like it in common scenarios. I hadn't heard its a know bad idea and am just curious. Thanks.
I think parent means that there are languages with scope-based clean-up (e.g. in rust/c++ a value will be cleaned up at the end of the scope that contained it, so one can even create a separate block inside a function) which is a better choice than forcing people to do clean up at the end of the function.
Note that Rust isn't dropping things "at the end of the scope" but at the end of their lifetime, it's just that if you declare local variables their lifetime ends when they fall out of scope and so this often (but not always, so it's worth remembering to care about lifetimes not scopes) coincides for values in those variables.
Making things more confusing, Rust is inferring scopes you never explicitly wrote, for example Rust brings a new scope into existence whenever you declare a variable with a let statement:
let good = something.iter().filter(is_good).count(); // good is a usize
let good = format!("{} of them were good", good); // a String
This is fairly idiomatic Rust, whereas it would sound alarm bells in a lot of languages because their shadowing is dangerous (if you hate shadowing you can tell Rust's linter to forbid this, but may find some other people's Rust hard to read so I suggest trying to see if you can live with it instead).
Obviously that first variable named "good" is gone by the time there's a new variable named good, and so that usize was dropped (but, dropping a usize doesn't do anything interesting, beyond making life harder for a debugger on optimised code since this "variable" may never really exist in the machine code). On the other hand the String in that second variable named "good" has a potentially long lifetime, if it gets out of this local variable before the variable's scope ends.
Because Rust is tracking ownership, it will know whether the String is still in good when that scope ends (so the String gets dropped), or whether it was moved somewhere else (e.g. a big HashMap that lives long after this stack frame). Because it tracks borrowing, it will also notice if in the former case (where the String is to be dropped) there are outstanding references to that String alive anywhere. That's prohibited, the lifetime of the String ends here, so those references are erroneous, your program has an error which will be explained with perhaps a suggestion for how to fix it.
If you do this with C++ destructors, they will sure enough fire at the end of the scope. Even if your String is long gone, the destructor fires anyway, destroying... a hollowed out String left behind to satisfy the destructor.
But go ahead and try it in Rust, your print doesn't happen because nothing was actually dropped. The String was moved, and so there isn't anything to drop.
NieDzejkob is correct: In Rust, shadowing a variable has no effect on when the destructor of the previous value runs. Thus there's no problem with retaining a reference to the previous value:
let a_string = String::from("foo");
let retained_reference = &a_string;
let a_string = String::from("bar");
dbg!(retained_reference);
dbg!(a_string);
Similarly, "non-lexical lifetimes" have no effect on when a destructor runs. The compiler will infer short lifetimes for values that don't need to be destructed (don't implement Drop), but adding a Drop implementation to a type will force every instance's lifetime to extend to end of scope. (Though as in C++, temporaries are still destroyed at the end of the statement that created them, if they're not bound to a local variable.)
The only exception to this rule that I'm aware of is what you mentioned about move semantics: Moving a value means that its destructor will never run. That's the big difference from C++. Everything else to do with destructors is very similar, as far as I know.
To my mind, move semantics being "the only exception" is a pretty bad joke. Unlike C++ Rust's assignment semantics are moves. So, you're not opting in to anything here as with C++ move, this is just how everything works.
For example, if you were to make the second a_string mutable, and then on the next line re-assign it to yet a third string containing "quux", the "bar" string gets dropped immediately, as a consequence of move semantics again.
In C++ you'd have to go write a bunch of code to arrange that, although I believe the standard library did that work for you on the standard strings - but in Rust that's just how the language works, you assigned a new value to a_string so the previous value gets dropped.
> In C++ you'd have to go write a bunch of code to arrange that
I don't think it's quite that bad. If you define a new struct or class that follows the "Rule of Zero (or 3 or 5)", the copy-assignment and move-assignment operators will have reasonable defaults. For example, the following Rust and C++ programs make the same two allocations and two frees.
Rust:
struct Foo {
m: String,
}
fn main() {
let mut x = Foo {
m: "abcdefghijklmnopqrstuvwxyz".into(),
};
x = Foo {
m: "ABCDEFGHIJKLMNOPQRSTUVWXYZ".into(),
};
}
C++:
struct Foo {
string m;
};
int main() {
auto x = Foo{"abcdefghijklmnopqrstuvwxyz"};
// Foo's default move-assignment operator is invoked on the temporary.
x = Foo{"ABCDEFGHIJKLMNOPQRSTUVWXYZ"};
}
The high-level "you assigned a new value so the previous value gets dropped" behavior is indeed what's happening, and it's automatic in most cases. But when we do voilate the Rule of Zero and override the default constructors/operators, things get quite complicated, and it's easy to make mistakes. (Also in general we often get more copies than we intended, when we're not dealing with temporaries.)
The "moves are implcit and destructive, and everything is movable" behavior in Rust is substantially simpler and often more efficient, and personally I strongly prefer it. But I'll admit that trying to contend with destructive moves without the borrow checker would probably be painful.
Since Microsoft has decided to sabotage C by not implementing anything that isn't in C++ already, and this will never be in C++, this feature is already dead.
Projects that target only GCC and Clang can use __attribute__((cleanup)) without waiting a decade for it.
> Since Microsoft has decided to sabotage C by not implementing anything that isn't in C++ already, and this will never be in C++, this feature is already dead.
Yes it is still true, they just have decided to support newer c standards when they sabotaged c11 by making optional a fundamental feature like complex numbers:
> Support for Complex numbers is currently not planned and their absence is enforced with the proper feature test macros.
Sadly, there is no future for the C language with a corporation like them in the committee. Much better to look at Zig or Nim, they fill more or less the same space and are developed by smart and passionate people.
> A compiler that defines __STDC_IEC_559_COMPLEX__ is recommended, but not required to support imaginary numbers. POSIX recommends checking if the macro _Imaginary_I is defined to identify imaginary number support.
They have not sabotaged anything when the feature is optional to start with.
If ISO wanted everyone to actually support it, it wouldn't be optional.
I'm interested. So defer is one of the handful of achievable goals I find Zig interesting and hold some hope for it. Easy to call C from Zig or Zig from C. And Zig can also compile C and target cross-platform easily.
const sprite_sheet_surface = c.SDL_LoadBMP("res/tile.bmp") orelse {
c.SDL_Log("Unable to create BMP surface from file: %s", c.SDL_GetError());
return error.SDLInitializationFailed;
};
defer c.SDL_FreeSurface(sprite_sheet_surface);
Somewhat related: I hacked some macro up in gnu89 C to get a Go styled defer once upon a time. I felt bad for making my compiler course instructor review that code... Very convenient, though.
An individual or handful of people make a proposal, the Working Group looks at the proposal, maybe it gets revised (this is version 2). Working Groups themselves do not, on the whole, produce documents like this.
There is no ISO magic ensuring working groups are all-knowing. There's an excellent chance that no members of WG14 have a deep in-depth knowledge of GCC, or that members who did they weren't particularly interested in that part of this paper (e.g. they were already staunchly for or against, remember ISO Working Groups are democratic, a proposal does not need consensus, so a working group member who thinks your idea is inherently good or bad might just skim the introduction, and move on)
C should be retired and replaced with compiled C# (if the usage allows for Garbage Collection) or Rust (if deterministic memory management is needed). There's really no use for pulling on a dead horse.
C has had its heyday and should simply curl up and die.
I think one difference between a classically trained programmer of a few decades ago and many of the programmers today who entered from javascript or bootcamps or were even self-taught is lack of understanding about all those other systems below you. For example, do you think the OP has heard of Simple Managed C?
C# is great, but it's not a systems language, it depends on piles of C/C++/etc code in order to run.
To be able to program in C# you don't need any .c file in your whole computer.
Of course you need a lot of binaries which were produced somewhere using low level languages in the process, and you probably need to comply with the C FFI to access a lot of libraries. But nothing that cannot be done with a different low level language.
You really on an entire stack that is programmed and maintained in languages like C, and to the degree that these are provided for whatever chipset you are using, yes you can code in C++. And of course you expect these libs to be regularly patched and updated, and released as new platforms become available, etc.
I'm not saying "don't code in higher level languages". I'm saying that not everyone can code in higher level languages. There is a whole stack that needs maintenance and development.
Not sure about bootcamps, but as a self-taught I have respect for C/C++ even if I do not use them. And even if I use Rust/whatever as self-taught I am especially humble because of all the knowledge I'm missing.
In 50 years everyone will have moved on to the hot new language, but your OS will still run, at least some, C code. Whether you like it or not C has enough inertia to last a really, really long time.
I don't like the nonlocality of this. When I see a closing curly brace, I want to be able to tell what execution will do when it gets there just by looking immediately before the corresponding opening curly brace, but now I'm going to have to look in the entire body of the block too. If I wanted things to be this implicit and nonlocal, then I'd have chosen C++ instead of C.
Except, when you start writing code with defers half your cleanup flags disappear, you end up with fewer branches and it makes it much harder to forget to free temporary allocations. It's really nice to look at only 1 place in code where something was allocated to ensure it also gets deallocated, instead of looking at the curly brace and then thinking: ok, what should be deallocated here, and when should it not be?
I don't like it. The great advantage of C is the very simple virtual machine and therefore very easy to reason about code. This bolting on language features is the hallmark of C++, which don't get me wrong has advantages, but in case we want the advantages we can already use C++.
The comment was about the generated code, not about arcane OS hacks that were introduced to add asynchronous notifications to otherwise blocking OS APIs.
So its better to make the control flow explicitly complicated with a bunch of labels and resource-flags and doing so inconsistently (because everyone has a slightly different view on how to implement it), than having the compiler do it in a consistent way?
Pragmatic point of view:
In 99% of cases, the developer doesn't care what the compiler produces and doesn't have to. A mechanism providing a complex control flow in the compiler output but is incredibly easy to read and reason about in the source code is useful in the vast majority of cases.
And for the 1% of cases where the programmer actually needs to know what the compiler produces, and / or full control over the control flow, the solution is simple: don't use `defer` and there, done, full control to the developer.
You don't need to code with "a bunch of labels and resource flags", though. There are most often good ways to clean up in a reasonable way. Even the simplistic malloc()/free() works with straight line code, just initialize to NULL and you can call free() without even checking that the resource was acquired. It's also often valid to not clean up at all because the OS cleans up after process exit.
And besides, if resources aren't released in the same function you'd need to come up with a different way to clean up anyway.
> There are most often good ways to clean up in a reasonable way.
Yes, and if a defer keyword were to be introduced in C, all these reasonable ways could still be used by anyone who wants to, while we could let the compiler handle it when we don't want to, making the source easier to read and reason about.
It's slightly less explicit, but at least it's still all explicitly contained within your function. For example, you don't have to open up the definitions for a dozen different classes and read their destructors to know what happens (if anything) when your function returns. (That's the kind of stuff that can make C++ nigh impossible to reason about.)
It seems barely more implicit than expression-3 in a for loop.
I dunno this capture stuff is alright but also a bit like blatant appeasement.
The point of this feature is to be non-cannonical RAII. I.e. it is about cleaning up a location. This data oriented approach is similar to how "locked data" is more correct and intuitive than "critical sections".
Now I get C being low level, so perhaps this is better, but I can't really imagine the thing I am cleaning up (as opposed to auxiliary info like an allocator to deallocate memory with) being captured by value.
BTW, speaking of moves, a "set this bool if I use this variable by value" feature would be very useful. Skip C++'s mistake and do Rust's model. Would work great with this feature.
> Now I get C being low level, so perhaps this is better, but I can't really imagine the thing I am cleaning up (as opposed to auxiliary info like an allocator to deallocate memory with) being captured by value.
I was wondering the same thing; what is the use-case for needing a capture-by-value option at scope exit?
Just make all captures a capture-by-reference and it works for all use-cases.
What an arrogant use of the word "simple" in the title.
* no decision on access to variables
* lambdas are involved at all
* "appearance in blocks other than function bodies [is] implementation-defined"
They cited D in the implementation, but then used Go as the inspiration for the feature. The answer was staring them right there in the face, scope(exit) [1]. Or, you know, they could have cited Zig for the exact syntax they wanted [2].
This feature is completely unusable.
Let me demonstrate. You can't use this inside an if statement:
if (foo) {
const ptr = malloc(...);
defer [&]{ free(ptr); } // implementation defined. get fucked
}
Furthermore, it's go-style, so the defer runs at the end of the function. This is not only implementation defined, it's full-blown undefined behavior because `ptr` goes out of scope before the defer expression runs.
Scope-based defer works great. Go-based defer is already problematic enough in Go; in C it's worse than not having defer in the language at all.
Point taken, but I will defend my claim: The few use cases where it does work are footguns because in the future, you or another collaborator will be tempted to wrap the code into a block, which is normally 100% safe for all other features of the language. It would be easy to do without thinking about it. But if you do it becomes UB as demonstrated above.
So the reasonable policy would be to use the same cleanup method everywhere, to avoid footgun firing when code is edited.
The Java AutoCloseable seems like a cleaner version of the MS __try/__finally:
try (MyCloseable c = useSomething()) {
// stuff
}
Why isn't this the path explored? The main difference would be not having extra nested blocks. That seems relatively minor, no? It can be improved arg list:
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.
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.
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.
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.
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 :)
Dunno, I feel like the goto out pattern is substantially more irritating any time you actually want to return a value from the function. I'd like to just return val instead of int val; /* ... */ ret=val; goto out;
244 comments
[ 3.1 ms ] story [ 281 ms ] threadRAII in C++ does not mean that all bytes are initialized, only the ones you want. In
only "header" and "footer" will be initialized: https://gcc.godbolt.org/z/4W1WzfPEvIt's not a matter of optimization, absolutely no compiler will zero-initialize unless you explicitely ask for it (or you are in a situation where the language mandates initialization, like global static in C and C++)
Oh man I'm about to ruin your day. Behold the horror:
https://stackoverflow.com/questions/29765961/default-value-a...
Thing I keep coming back to is the proportion of effort you have to spend learning and keeping up with your tools vs the problem domain your working on. Feels like a big problem with C++ is how much energy the language itself uses up.
A friend of mine is, unlike me, really really smart. He likes golang and hates C++. And not like he hasn't spent years professionally writing C++ code. I think the advantage of golang is he can write it reflexively. So all of his attention is on the problem not the language.
Me I feel like the problem with C is not so much the language. It's that I'm always worried about stepping off a ledge. Things like defer would help. Because failing to clean up resources is a big problem with C.
I think most of my functions that allocate a resource only release it on error; otherwise the allocated resource lives on and gets freed later by another function. Defer doesn't help at all here. It just helps with the trivial ones..
The SO post looks confused ; A, B, D, E and F are the exact same case wrt the initialization of the int. It does not matter that there's a constructor or not and which shape it has, only that the variables get initialization somewhere or not (and that can be in the ctor's member init list, in the struct définition or even when creating a value if aggregate-initializing).
While the first draft was released in 1983, the first ratified C standard was in 1989.
The first edition of "The C Programming Language" (from which I first learned the language) was published in 1978, over a decade before the language was standardized.
You are correct that C89 added function prototypes. However, contrary to your implication, C borrowed the concept and syntax from C++.
There's a notable difference between C's prototypes and C++'s ... `int foo();` in C is not a prototype, whereas in C++ it's equivalent to `int foo(void);` ... C had to maintain compatibility with K&R style declarations, whereas C++ didn't.
I've never seen the advantage to having a block structured defer: it's easy to add a new function, it's not always possible to remove a block.
Go can allocate defers on the heap, but that's a different story.
The pthread_cleanup_push/pthread_cleanup_pop thing presumably keeps its own heap-allocated vector, backed by malloc or something. C itself can't willy nilly heap-allocate, so that list will be on the stack. But the stack is tiny compared to how long loops can be. Hence stack overflow.
Given a possible implementation, this loopy mutex example could have a tiny stack footprint: a single pointer to the shared cleanup() function; and a single pointer to the head of a (heap allocated) linked list of pointers to mutex (i.e., the function arguments). And the function pointer would not necessarily require allocation at all, as we can statically point at the function definition here. So we are down to a single word of stack allocation.
In the general case I see no alternative to either the compiler generating one alloca() per defer or heap allocating defer callbacks. Both are terrible solutions for C, because alloca can overflow, while heap allocations can fail with an error code and defer has no way to catch that error. Besides, C programmers just won't use the feature if it requires allocation out of performance concerns. Block-scoped defer is the only reasonable semantics.
> C programmers just won't use the feature if it requires allocation out of performance concerns.
I agree that many C programmers wouldn't touch the feature for performance reasons. But let's not pretend that every C program is a video driver, a AAA game or a web engine. Many, many large C programs would benefit immensely from `defer` semantics -- otherwise, why would the GCC feature exist -- and they are performance-tolerant enough that a little heap allocation would be a reasonable tradeoff for increased safety.
But I'm not really defending `defer` in the first place...
> Block-scoped defer is the only reasonable semantics.
I agree with you completely. :) I was never defending function-scoped `defer`, but answering claims about the necessity of stack allocation. There are possible defer implementations that wouldn't blow the stack: that's my only point.
While true, the culture in C and C++ circles pretends otherwise, hence why we have unsafe by default and safety as opt-in in those languages.
The sky would fall if we lose that 1us.
It's healthy for culture-conscious programmers to reflect, now and then, on just how small a segment they represent. Cultured programming is fine, but it's like opera: the ordinary programmer recognizes a few of the tunes, but they don't sing along. It's hard to appreciate just how much uncivilized business code is out there, when nobody is getting HN likes for keeping that 1980's ERP running.
Nevermind embedded platforms where heap might be unavailable or just so scarce that its use beyond early initialization is strictly verboten. Or interrupt handlers where you simply can't call an allocator.. Block scoped defer could still be useful on such systems (e.g. with locks).
...But as I pointed out in a sibling comment, I was never defending function-scoped defer. I agree with you. I was pointing out that the implementation of such a feature wouldn't require excessive stack allocation.
But I looked through the spec again, and it actually just says that defer outside the top level is implementation-defined, so this is irrelevant anyway.
This has been a fun conversation, and I really enjoyed chatting with you about this. :) Take care.
that looks like a super big gotcha, hasn't there been enough bugs with alloca() being called in a loop yet to show how risky and unintuitive this is ? block-scoped is explicit and explicit is good
If I had to choose between allowing that pattern and allowing reasonable usage in all control constructs, I'd choose the latter.
Also, Wikipedia (https://en.wikipedia.org/wiki/Deadlock, https://en.wikipedia.org/wiki/Deadlock_prevention_algorithms) doesn’t seem to know about it. Even though I expect/guess it to be popular in embedded work, that makes me wonder whether lock ordering is used much.
Could also be an omission in Wikipedia. It has https://en.wikipedia.org/wiki/Banker%27s_algorithm, which I hadn’t heard of, and that Wikipedia says of
“In most systems, this information is unavailable, making it impossible to implement the Banker's algorithm. Also, it is unrealistic to assume that the number of processes is static since in most systems the number of processes varies dynamically”
Function-scope defer is something that surprises everyone I explain it to. Programmers naturally expect defer to be block-scoped.
As far as I know, most functions have only one defer, and Go optimizes such cases quite trivially, by inlining calls to the deferred function at every function exit at compile time, without managing an additional stack. If there are several defers, then yes, the slow path is used.
Is it worse than in C that there is no concept at all? Is it better that everyone is on their own to do it their own way every time?
I’m not above making the eaiser, but to your point when I see their example of:
That doesn’t look like the C that I know.What's the alternative, though? Sprinkle your code with try..finally's? RAII like in C++?
Also why is RAII bad? It's an awesome feature in C++.
I didn't mean it's bad (I used to be a C++ developer myself and enjoyed RAII a lot), just wondering what are the alternatives that the OP doesn't consider "hacks". RAII would require to introduce constructors/destructors in the language, with all the gotchas (and probably you'll want a full-fledged OOP after that), which is apparently against Go's design principles as a simple language.
>Auto-Closable interface with syntactic support.
I don't see much difference here in practice; the whole difference is that in C#, for example, you use "using" on a whole object, while in Go it's a "defer" on a specific method of the object (or a standalone function). You are not limited to a single method and can use it on any method you deem necessary.
Auto-closeable/RAII, however, is less flexible in ad hoc situations specific to a certain function (you have to define dummy classes just to make sure a function is called no matter what), Go allows to use "defer" on a lambda. Auto-closeable also ties control flow to an object, which makes sense in an OOP-focused language, but Go isn't one.
Otherwise a generic _bracket_ operator could be introduced, similar to Python's `with` statement. In C it might be a bit hard to parameterize, but even if it just requires you to use a void, it's still an improvement vs. not having anything at all. (Talking about language features so `__attribute__((__cleanup__))` doesn't count.)
Imagine something like this:
Edit: After writing this sample I could even an optional `else` block in case `fopen` returned `NULL`.Edit2: Of course, syntax fine-tuning would be welcome. For instance, `handle` should be restricted to the bracket's scope.
Zig has errdefer which only runs if an error occurred below the statement within that scope. It allows you to always keep cleanup locally, but you can still handle errors for your business logic somewhere else.
errdefer is cool, but I don't think C should support it. Mainly because you'd need to spec errors at a language level, and doing that today is probably impossible.
The only other proposal I'm more interested in are the lambda approaches: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2890.pdf
The point here is that the C working group should be standardizing existing practice and helping the existing users of C, and not striking out making bizarre new syntax choices and keywords which are completely unproven.
They should have made it so that cleanup took an expression block:
Or at least allow a statement expression returning a compile-time known function pointer: Sure, it still looks ugly, but at least a single generic macro would be able to clean it up and make it look better.Or you could just put up with C, like everybody else does, and be quietly thankful that the myriad layers of our modern, complex systems are being maintained by other people.
(Or, since the standard would be creating new names, have "cleanup" and "cleanup_ptr" instead. Assuming that this is a separate attribute namespace, which I believe it is[?])
FWIW, you do sometimes need the address of the variable. Particularly if it's some struct that is made a member of some container (e.g. linked list) temporarily - you need the original variable's address to unlink it.
> This indicates that existing mechanism in compilers may have difficulties with a block model. So we only require it to be implemented for function bodies and make it implementation-defined if it is also offered for internal blocks.
How will that work?
Anyway, this would be great to have in C as it would simplify how resources are handled.
It would be a shame if this defer was limited to function scope. It would be very useful in nested blocks as well. But, I would still appreciate it.
defer and auto are the only things I would love to see in C.
Why "on function exit" style defers - already known to be a bad idea from Go - is beyond me. Such a solution more or less requires storage of potentially unbounded allocations.
I would sort of understand if this was proposed for a high level language garbage collection where actual memory usage is secondary.It doesn't matter what the defer does, if it's allocated in a for statement and doesn't go off until the surrounding function exits, then it's going to have to stuff tons of function pointers and parameters somewhere, presumably alloca'd onto the stack over and over.
That's not C. That shouldn't be C.
There are a dozen languages for doing clever dynamic magical things in code. C is still fairly straightforward. Use one of the other languages instead of bagging down C with complexity until it turns into another difficult C++ variant over time.
Is there something you can point me to about this? I write Go professionally and from a readability and utility standpoint I really like it in common scenarios. I hadn't heard its a know bad idea and am just curious. Thanks.
Making things more confusing, Rust is inferring scopes you never explicitly wrote, for example Rust brings a new scope into existence whenever you declare a variable with a let statement:
This is fairly idiomatic Rust, whereas it would sound alarm bells in a lot of languages because their shadowing is dangerous (if you hate shadowing you can tell Rust's linter to forbid this, but may find some other people's Rust hard to read so I suggest trying to see if you can live with it instead).Obviously that first variable named "good" is gone by the time there's a new variable named good, and so that usize was dropped (but, dropping a usize doesn't do anything interesting, beyond making life harder for a debugger on optimised code since this "variable" may never really exist in the machine code). On the other hand the String in that second variable named "good" has a potentially long lifetime, if it gets out of this local variable before the variable's scope ends.
Because Rust is tracking ownership, it will know whether the String is still in good when that scope ends (so the String gets dropped), or whether it was moved somewhere else (e.g. a big HashMap that lives long after this stack frame). Because it tracks borrowing, it will also notice if in the former case (where the String is to be dropped) there are outstanding references to that String alive anywhere. That's prohibited, the lifetime of the String ends here, so those references are erroneous, your program has an error which will be explained with perhaps a suggestion for how to fix it.
But go ahead and try it in Rust, your print doesn't happen because nothing was actually dropped. The String was moved, and so there isn't anything to drop.
https://gist.github.com/rust-play/4bbcc2a4efb641e578e84a1962...
The only exception to this rule that I'm aware of is what you mentioned about move semantics: Moving a value means that its destructor will never run. That's the big difference from C++. Everything else to do with destructors is very similar, as far as I know.
For example, if you were to make the second a_string mutable, and then on the next line re-assign it to yet a third string containing "quux", the "bar" string gets dropped immediately, as a consequence of move semantics again.
In C++ you'd have to go write a bunch of code to arrange that, although I believe the standard library did that work for you on the standard strings - but in Rust that's just how the language works, you assigned a new value to a_string so the previous value gets dropped.
I don't think it's quite that bad. If you define a new struct or class that follows the "Rule of Zero (or 3 or 5)", the copy-assignment and move-assignment operators will have reasonable defaults. For example, the following Rust and C++ programs make the same two allocations and two frees.
Rust:
C++: The high-level "you assigned a new value so the previous value gets dropped" behavior is indeed what's happening, and it's automatic in most cases. But when we do voilate the Rule of Zero and override the default constructors/operators, things get quite complicated, and it's easy to make mistakes. (Also in general we often get more copies than we intended, when we're not dealing with temporaries.)The "moves are implcit and destructive, and everything is movable" behavior in Rust is substantially simpler and often more efficient, and personally I strongly prefer it. But I'll admit that trying to contend with destructive moves without the borrow checker would probably be painful.
...of the loop? Call a function.
Projects that target only GCC and Clang can use __attribute__((cleanup)) without waiting a decade for it.
This is no longer true: https://devblogs.microsoft.com/cppblog/c11-and-c17-standard-...
> Support for Complex numbers is currently not planned and their absence is enforced with the proper feature test macros.
Sadly, there is no future for the C language with a corporation like them in the committee. Much better to look at Zig or Nim, they fill more or less the same space and are developed by smart and passionate people.
They have not sabotaged anything when the feature is optional to start with.
If ISO wanted everyone to actually support it, it wouldn't be optional.
> This indicates that existing mechanism in compilers may have difficulties with a block model.
Erm. No. GCC's cleanup attribute is attached to nested blocks. Whatever that means for the conclusion there.
How is a pretty basic misunderstanding like this seeping into an ISO WG document?!?
[add.:] in case anyone wants to double check: https://gist.github.com/eqvinox/c062f5a46f3a60b1151fcdf3e91a...
There is no ISO magic ensuring working groups are all-knowing. There's an excellent chance that no members of WG14 have a deep in-depth knowledge of GCC, or that members who did they weren't particularly interested in that part of this paper (e.g. they were already staunchly for or against, remember ISO Working Groups are democratic, a proposal does not need consensus, so a working group member who thinks your idea is inherently good or bad might just skim the introduction, and move on)
C has had its heyday and should simply curl up and die.
people don't understand what "programming" is about anymore
C# is great, but it's not a systems language, it depends on piles of C/C++/etc code in order to run.
Of course you need a lot of binaries which were produced somewhere using low level languages in the process, and you probably need to comply with the C FFI to access a lot of libraries. But nothing that cannot be done with a different low level language.
I'm not saying "don't code in higher level languages". I'm saying that not everyone can code in higher level languages. There is a whole stack that needs maintenance and development.
Not in the standard, but there are compiler implementations (like Microsoft’s) that have added it.
sleuthkit/scalpel
microsoft/service-fabric
dotnet/runtime
microsoft/Detours
dotnet/wpf
Chuyu-Team/VC-LTL
apache/logging-log4net
microsoft/Windows-classic-samples
microsoft/winfile
dotnet/llilc
microsoft/DirectXShaderCompiler
dotnet/diagnostics
SoftEtherVPN/SoftEtherVPN
microsoft/PTVS
aybe/Windows-API-Code-Pack-1.1
I don't think that it's in any way meaningful, it's literally unused given that it's been there for likely 20+ years.
There is a reason why it was introduced in Go.
Pragmatic point of view:
In 99% of cases, the developer doesn't care what the compiler produces and doesn't have to. A mechanism providing a complex control flow in the compiler output but is incredibly easy to read and reason about in the source code is useful in the vast majority of cases.
And for the 1% of cases where the programmer actually needs to know what the compiler produces, and / or full control over the control flow, the solution is simple: don't use `defer` and there, done, full control to the developer.
So where exactly is the downside?
And besides, if resources aren't released in the same function you'd need to come up with a different way to clean up anyway.
Yes, and if a defer keyword were to be introduced in C, all these reasonable ways could still be used by anyone who wants to, while we could let the compiler handle it when we don't want to, making the source easier to read and reason about.
It seems barely more implicit than expression-3 in a for loop.
I don't feel like it's going too far for C.
The point of this feature is to be non-cannonical RAII. I.e. it is about cleaning up a location. This data oriented approach is similar to how "locked data" is more correct and intuitive than "critical sections".
Now I get C being low level, so perhaps this is better, but I can't really imagine the thing I am cleaning up (as opposed to auxiliary info like an allocator to deallocate memory with) being captured by value.
BTW, speaking of moves, a "set this bool if I use this variable by value" feature would be very useful. Skip C++'s mistake and do Rust's model. Would work great with this feature.
I was wondering the same thing; what is the use-case for needing a capture-by-value option at scope exit?
Just make all captures a capture-by-reference and it works for all use-cases.
* no decision on access to variables
* lambdas are involved at all
* "appearance in blocks other than function bodies [is] implementation-defined"
They cited D in the implementation, but then used Go as the inspiration for the feature. The answer was staring them right there in the face, scope(exit) [1]. Or, you know, they could have cited Zig for the exact syntax they wanted [2].
This feature is completely unusable.
Let me demonstrate. You can't use this inside an if statement:
Furthermore, it's go-style, so the defer runs at the end of the function. This is not only implementation defined, it's full-blown undefined behavior because `ptr` goes out of scope before the defer expression runs.Scope-based defer works great. Go-based defer is already problematic enough in Go; in C it's worse than not having defer in the language at all.
[1]: https://dlang.org/spec/statement.html#scope-guard-statement
[2]: https://ziglang.org/documentation/0.9.0/#defer
So the reasonable policy would be to use the same cleanup method everywhere, to avoid footgun firing when code is edited.
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.
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.
This is a rationalization, and a bad one. When your solution is "just pull in another programming language", you have a problem.
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.
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.
So the earliest C compilers were under 5000 lines of C+asm:
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: 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.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.
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.
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 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.
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.
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.
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.