However, this is not without precedence in C. For example, just look at the for loop:
for (clause1; expression2; expression3) statement
expression3 is executed after statement.
I think the best defense of this syntax is that it makes writing basic iteration a bit nicer without having to add boilerplate (the iconic `for (i = 0; i < n; i++)`) but then I would argue that the real problem is that C is severely lacking in the iteration department and this is a rather obvious hack (that languages like Javascript felt the need to copy wholesale, for some insane reason).
Just like every C feature: if you know how it's implemented (and optimized), you will know what is going to happen. All big C compilers already support stack frames and unwinding, so it's not even entirely novel functionality.
While you may object it's not entirely obvious how unwinding is going to be implemented, OTOH in functions with complex control flow it can be easier to understand which `defer`s are going to be run, as opposed to following nested `else` statements, or reasoning about the program state from all `goto cleanup` locations.
With macros this is not exactly true. I gesture towards the GObject system for an extremely complex situation in big important production software.
There's nothing implicit about defer.
That's quite implicit. You can no longer reason about a local piece of code; you now have to know its lexical nesting up to top level to see if it's inside a guard block that might trigger hidden behavior.
Plus, to handle the free or the leak if you had forgotten to free a resource at the exit point would also require to "know its lexical nesting up to top level".
Between goto and longjump and co, C has much worse non-local behavior than defer.
By your definition only [1] "come from" would be non-local.
You can't goto out of a function, and you know there's exactly one such label inside it. If goto isn't local, then neither are function calls, since the function could be defined anywhere in the codebase.
You can with a goto expression and a label address available - though the behavior is undefined in C, so bets are off.
And you can with longjump/setjump more explicitly.
You have to check every line of a while loop to know what’s going on. Heck, another thread could hold a pointer aliasing the loop variable and then mutate it, causing the loop to terminate for no obvious reason.
Anyway, what could possibly happen at the end of a while loop, besides going back to the begining for the test?
Besides, if you have a bug in your code, you will have to look at the whole block anyway.
Which C doesn't have.
They are no less explicit than the actions performed at the end of iterations of for or while loops. In general, for C, understanding the behavior of code requires understanding “is it in a block, and if so what kind of block”; guard blocks would be not generally different.
However, for some things you can only free/return resources if you successfully created that resource. At which point you would need to use something like a stack.
That said, it's still debatable if it's useful, given that you can achieve the same thing with the
struct some_resource resource;
do {
resource = allocate(...);
if (!resource) break;
} while(0);
if (resource) dispose(resource);
I can see it as a good thing because you have the dispose statement next to the allocate statement, which makes the logic easier to follow, but the implementation may have caveats which make it actually harder to reason about, e.g. see the other thread about capture value vs capture reference - C will most probably need to capture by reference, which means that modifying "resource" later on changes the meaning of the deferred statement.This has not been true in many years. C appears to be such a language, but optimizing compilers have learned how to find and optimize undefined behavior in code that most C programmers don't realize is unsafe. As a result there can be a considerable gap between the code as clearly intended, and the code that will be generated.
See https://blog.regehr.org/archives/213 for more on this. Including real world examples of things like validation checks being elided by the compiler, leading to real-world vulnerabilities in programs which clearly have checks to avoid exactly those vulnerabilities.
A possible explicit solution I can think of is to introduce a new function attribute that gives the name of their ‘cleanup’ function (so that the compiler would know fopen needs a fclose, for example) and a compiler that uses these attributes to issue a warning if a function has a path that calls a function and doesn’t either call its cleanup function or returns its result.
I don’t know whether that would cover all bases, though.
But more that there is so much magic and abstractions that it is very hard for a dev to have a clear view of what is going on and what to expect. He has to 'guess' instead of just read the code. For that, I guess that it is similar to the current question of accountability of decisions made with deep learning algorithms.
As an example of my point, I would refer to the 'garbage collection' issues of a language like 'java'. GC will happen at a logically defined point like 'dirty mem > 100m' but from the developer point of view, his logic could suddenly lag unexpectedly in a middle of a simple operation because the GC was triggered by internal magic. It is very hard for a dev to be able to determine the memory usage at different points of the code and so have a certitude of when this operation could happen.
Two languages could hardly be less alike than Java and C++ - the latter is not garbage collected and where destructors are called is completely predictable.
It is more deterministic in C++, but you can still call exit(). (which is even more straightforward than the refcount cycle)
in the specific case of shared_ptr, which are a small part of codebases, if they are even used - for instance an immense amount of C++ GUI programs use Qt which doesn't use shared_ptr-like ownership semantics but instead a tree-of-objects model which does not have this issue. In contrast in Java / C# any object that has a reference to another is at risk.
when I put some object on automatic storage in C++ I do so because I explicitely want it to go away when its scope is left (either by reaching } or through an exception)
It's not really random, but it's quite hard to follow.
(std::shared_ptr should be used only when absolutely required and should be kept under close watch the whole time.)
Should I be alarmed?
If you have a situation where a value needs to be shared between multiple other values, but also the value is trivial enough that it doesn't matter when it's destroyed or what it does when it's destroyed, but also the value is not so trivial that you can copy it instead of using a refcounting pointer, then std::shared_ptr is fine.
Unless you know you must share objects with multiple potential owners, though, std::unique_ptr is a lot wiser.
>but also the value is trivial enough that it doesn't matter when it's destroyed or what it does when it's destroyed"
that precludes the value from having other refcounting pointers that could then create cycles and leak.
There’s a good chance that most of the shared_ptr’s can be replaced by unique_ptr, which has less runtime overhead. More importantly, it documents the intent of the programmer regarding ownership semantics, and the timespan during which the object should be remain valid.
I would suggest functions like atexit and pthread_cleanup_push as well established counterexamples. Granted this is not a perfect comparison because these are implementable without extending the c language; however, I think they have the same general idea of "defering" cleanup. I think the proposed defer functionality is actually more readable because the defer command will be written much closer to where it will be exited. Compare this to atexit() which may be put anywhere.
Probably. It's one of Go's lesser ideas.
C already has a "defer" mechanism in "exit", to close out files and such. Of course, the final I/O status gets lost.
Oh yeah?
I don't agree! There's a lot of behavior that's implicit and you essentially need to internalize the C standard/compiler behavior to follow. Weak typing, for instance; defaults for memory access/fencing, handling faults, runtime semantics with respect to initialization of the process, cleanup via atexit, and signal handling.
Memorable, sure, but hardly explicit.
What is "I am thinking of using C?"
One of the key interest of C compared to more recent languages is that everything is explicit. With a finger you can follow the code as it runs and know exactly what is going on and when exactly.
At the opposite, there is c++ that does a lot of things automagically. And it is often hard to understand why you suddenly get a segfault out of blue, just because a destructor was randomly called at the wrong time.