3 ms·
At least for python, using is not more limited, but more complete: It allows to handle exceptions that are raised in the block and it also allows you to raise e
by chronial 6y ago
At least for python, using is not more limited, but more complete: It allows to handle exceptions that are raised in the block and it also allows you to raise exceptions in the "destructor".
That is something that you actually need rather frequently. For example a db transaction wrapper might want to rollback instead of commit if we leave the block via an exception. You also want the "destructor" of that block to raise if the transaction can't be committed.
The only thing that the c++ method does really well is memory management (e.g. unique_ptr), because free() can't fail. But that use case is of course not a thing in gc languages.
Regarding the composition: Everything that's not memory management usually has side-effects and often wants to do something with regards to exceptions. So it's a good thing for the caller to know that the "RAII" is happening. Allowing you to complete hide that in your class is not a benefit.
C++ paints a distorted picture here, because its method is great for memory handling issues, but only OKish for everything else. But most of your RAII issues in C++ are memory related, so you don't notice the problems that much.
- jcelerier 6y ago> At least for python, using is not more limited, but more complete: It allows to handle exceptions that are raised in the block and it also allows you to raise exceptions in the "destructor". the difference is that with Python, the programmer can forget to use "using". In C++, as soon as you instantiate your variable on the stack somewhere, you know that ~T() will happen unless you get a segfault... > For example a db transaction wrapper might want to rollback instead of commit if we leave the block via an exception. I'd say that a transaction wrapper would have an explicit commit method which applies the transaction and "empties" the transactoin object - destructor would always rollback if there is something that was not committed - and if commit was called then there is nothing to rollback anymore so ~T() does nothing.
- geofft 6y agoThe way you'd do this in Python is something like @contextlib.contextmanager def get_a_locked_thing(): with lock: yield thing and then with get_a_locked_thing() as thing: do_stuff_with(thing) Exiting this "with" block causes get_a_locked_thing to resume at its yield statement, which exits that "with" block, which releases the lock. Throwing an exception also exits the context managers. If you were to try omitting the "with" statement and running, say, "thing = get_a_locked_thing()", the function hasn't executed yet (because you haven't entered the context manager), meaning that not only is 'thing' the wrong object, you also haven't even gotten the lock yet. So you won't deadlock/leak the lock, and you will notice in the most basic of tests that your code isn't doing the right thing. I do agree that this is nowhere near as nice as having an object in a local variable, because "with" adds an extra layer of indentation, and that's an advantage of C++/D/Rust-style RAII. But it's definitely doable in Python and pretty idiomatic.
- quietbritishjim 6y agoAh good point, that is perfectly doable. But even accepting the explicit with statements, it's still not as composable in more general situations. For example, I don't see how to reliably pass an active context manager to a function for it to unlock part way through its execution, or return one of two active context managers (if you tried using your technique then both would still be active if you couldn't exit the undesired one before the yield). But maybe you're right that these are a bit less common, except for memory management, than I'm imagining.
- quietbritishjim 6y agoThe one example I gave in the parent comment already addresses everything you talked about. Yes you can use a Python context manager (that's what you're referring to) to lock a mutex, but you can't pass that context manager as a return result of a function and be sure its __exit__ method will be called appropriately in the three situations I mentioned (immediately if result thrown away, at the end of the function if stored in a local variable, later still if returned from that outer function). In fact I don't think you can return a context manager from within a "with" block at all, even accepting that the parent function will have to manually reuse it in another with block, because its __exit__ method will already be called in the inner function. Technically this is true in C++ too of course because the object in the inner function will have its destructor called, but its move constructor will be called first giving you an opportunity to clear out its internal state (this is why my example used a unique_lock rather than a lock_guard). Things are even better in Rust: the bytes of the object are directly copied to the new object's footprint and the original destructor isn't called, so you don't even need to set up a dummy "empty" state. > The only thing that the c++ method does really well is memory management (e.g. unique_ptr), because free() can't fail. Mutex unlocking usually can't fail (and if they do, there's usually not much you can do about it). Same with closing network connections. Closing files can fail, and for some programs it's very important to know when that happens, but for many others it's not important at all. Python's chained exceptions are very nice, but not so important that their absence from C++'s exceptions (or Rust's panics) makes those languages useless.
- gpderetta 6y agoRAII is great for transactions. In a proper robust design, commits should always be explicit and destructors only rollback uncommitted transactions. If your rollback can fail, well, then you have bigger issues. There is now enough introspection in the language to actually implement implicit commit in a non exceptional path, but I think it would be a mistake.