RAII doesn't help with a generic pair of init and deinit functions, such as malloc() and free() for example, unless you wrap your mallocs into "memory objects" or something. You can't do anything with RAII unless you wrap your stuff into objects which just forces the OO crap on everything regardless of whether it's a good fit for the OO paradigm.
But even in RAII, classes are merely just a way to bind init and deinit together. As a result, you can't "forget" to free the resource. (Unless you use the new operator...) But this is merely an interface issue: defer could be of the form
void *block = defer malloc(SZ) with free(block);
or something similar that requires the init and deinit calls to be part of the defer expression itself.Nevertheless, a defer is clearly a useful language feature that C actually lacks. Currently C doesn't offer the code any attaching points to the lexical scope of the program. You can fake it with a special for(;;) statement but it's a kludge and even by wrapping it into a macro it's very hard to make it a generic solution.
people have been writing RAII scope_exit classes for at least 20 years to do exactly this.
> You can't do anything with RAII unless you wrap your stuff into objects which just forces the OO crap on everything regardless of whether it's a good fit for the OO paradigm.
wrapping something in an object does not make it OO. For example there is nothing object oriented about std::unique_ptr.
c'mon, here's the article from 2000 that introduces ScopeGuard : https://www.drdobbs.com/cpp/generic-change-the-way-you-write...
That's Microsoft's fault; it's 2020 already, and unless things changed since I last looked, their compiler still doesn't have full support for the C99 standard.
Even then, some of the C99 syntactic features have been somewhat widely adopted (at least when not compiling for Windows); off the top of my head, we have "//" comments, declarations in the middle of a block, and designated initializers.
Everything that was moved into optional in C11, like VLAs, is not planned to ever be supported.
I see people tend to write C89+designated initializers but this is a code smell IMO. Initial data should always be 0, especially data that lives in BSS/data section.
If anything RAII is unfit for C (which doesn't have classes), and in itself, a kludge (it's an idiom, not a language feature).
So it's immediately better compared to the current C situation.
As for compared to RAII? Well, thats one failure mode for defer (forgetting it), whereas there are dozens of ways to mess RAII...
In comparison to RAII, “defer” as a language feature is a kludge. Mainly because RAII completely solves the problem of correct resource cleanup for API users while “defer” does not.
So I don't see the comparison...
Destructors probably cannot be usefully imported into C while preserving the simplicity of the language: then you'll want at least unique_ptr to manage your memory with destructors, then probably shared_ptr, and some ownership semantics as you pass those around, et cetera. In that sense, defer strikes a balance between simplicity and usefulness. However, there is still a comparison.
RAII is the best for ensuring that things get cleaned up even if it does lead to more boilerplate to add the pattern to things. But even .NET's IDisposable feels better than defer. It's established practice to implement it if your class contains something that must be disposed. And it's easy for tools to check if you haven't disposed of something that implements IDisposable.
All that said, I find defer to be better than nothing. In C, the best I can do for cleanup is having all the cleanup functions called at the end with labels at the various points I need to jump to.
In terms of usability, destructors allow for resource management that is far easier at the call site than any other language. "with" blocks such as in python require the call site to be modified to include the explicit time of destruction, whereas C++ gets that for free from its existing scoping rules. "try/finally" such as in Java also requires modifying the call site, but at least has reasonable default behavior if accidentally omitted. "defer" feels like it has the worst of both worlds. It requires the call site to be modified when a resource is owned, and also requires the caller to know what the corresponding cleanup function is for any allocation.
For example, I cannot write:
lock (mutex);
Expecting to declare an anonymous lock with the mutex passed in, as this gets parsed as a declaration of a lock called mutex (hopefully lock doesn’t have a default constrictor and I at least get an error). Instead I have to bake my lock: lock l(mutex);
Often I have objects that only exist for RAII and it’s a little bit annoying to have to give them dummy names.You can do
lock(mutex), some-lock-protected-expression;
but it is a bit too cute and limited.On the other hand, being able to name the RAII object is often very useful for example if you need to dismiss them early, which happens often in transactional code (unlocking a mutex before the end of scope is a relatively common occurrence).
Also specifically for mutexes, a nice pattern is to bind object and mutex together to guarantee that the object is always used with the lock:
sync<my_object> foo;
{
auto l = foo.lock(); // starts the critical section
l->do_something()
l->do_something_else();
} // it ends here
// or if you only need to hold the mutex for a single call:
foo->do_something(); // critical section lives only for the full expressionI didn't say it wasn't. Being able is perfectly fine, being forced is not. As I mentioned in another comment, the problem isn't the extra typing, the problem is that it requires us, the programmer, to remember to name it even when we don't feel we need and to remember that "foo (bar)" isn't calling the constructor of foo and passing in bar, its calling the default constructor and declaring bar. That's too many gotchas and rather error prone!
I believe there was a cppcon talk where the speaker said that it was a very common bug in Facebook, despite that they have linter rules to catch this case. Its also a rather insidious bug, because the code will run seemingly normally, just... it never actually locks anything. A rather hard problem to debug too.
Actually, I think it's worse than that; IIUC, it will block until the mutex is unlocked, then run. Under light load, it will get away with this, but if the load is heavy enough, it will either guarantee that every process waiting for the lock runs at once, trampling each other's work, or almost-serialize them, making the race condition even more intermittent than if they didn't lock at all. Which failure mode you hit depends on how your scheduler works.
#define LOCK(mutex) lock lock##__LINE__(mutex)
Or even C compatible scoped locks: #define SCOPED_LOCK(mutex) \
for (int i##__LINE__ = lock_mutex(mutex), 1; \
i##__LINE__ --; \
unlock_mutex(mutex))
Use like SCOPED_LOCK(foo->mutex) {
do_stuff();
}The problem I'm describing is less about the effort of adding a variable name -- that's really not a big deal -- its that its super error prone to remember and these bugs slip through all the time. Requiring a variable name (either explicitly or through a macro like yours) is error prone.
With lock() being a function taking a lambda.
lock([]{
// code locked with anonymous mutex
});Could u point to an example of programmers doing this ?
Although I guess Cheney on the MTA counts.( https://dl.acm.org/doi/10.1145/214448.214454 )