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).
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).
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
});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.