C# has "using", Java has try-with-resources, Python has the "with", etc. - all of which support RAII patterns.
C# has "using", Java has try-with-resources, Python has the "with", etc. - all of which support RAII patterns.
std::unique_lock<std::mutex> foo();
The unlocking of the mutex is then tied up with what you do with the returned mutex locker e.g. if you ignore it then it will be unlocked at the end of the current statement, if you assign it to a local variable (and don't move from that) then it will be destroyed when leaving that enclosing function, or you return it from that enclosing function to extend the length of the lock still further.You also can't compose those mechanisms so easily. For example I could make a class with a std::unique_lock<std::mutex> and a pointer to the data it protects and pass that around.
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.
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.
@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.
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.
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.
auto my_leaky_mutex = new std::unique_lock<std::mutex> {foo()};
Yes, I am making it fail on purpose, but copy-paste in enterprise codebases does wonders for code quality.I don't know what processes Google uses for the Android codebase, but in the main code base make_shared has only been available for a little while. Today, "new" is smell that should be caught in code review at Google. When was this committed?
So what to be expected by those companies that hardly have such a deep knowledge about C++ among their troops?
This is pretty much what we do in our code base, except that the class has lock(), tryLock(), etc functions that take a lambda. The functions acquire/try to acquire the mutex and then pass a reference with appropriate const-ness to the lambda.
RAII is strictly more powerful and more regular than context managers.
[1] if you are implementing something like a container and dealing to raw memory you might need to call destructors of subobjects explicitly, but because it is needed to correctly reclaim memory, it is almost always done and all standard containers do it already. On the other hand in python list doesn't implement the context manager protocol.
https://android.googlesource.com/platform/frameworks/base/+/...
they should have used make_shared, but it is still safe
It exposes everything as C APIs, even thought it is a mix of Java and C++ on the implementation side, leaving the C++ developers to write their own type safe abstractions from scratch.
Most just can't be bothered and write C in C++.
Then there is the whole issue how, following Google C++'s guidelines, the NDK is actually implemented.
I don't know how much back in the past it is, but pretty much every time in the last ten years I tried to beat a C++ abstraction, I almost never managed to substantially improve (and since gcc.godbolt.org exists it made it much easier to witness that most of the abstraction layers entirely disappear at -O2)
Or they may not see that stuff as an improvement in the first place (e.g. see Orthodox C++[0]).
Discomfort is not a valid reason to avoid learning new things.
That seems like a rather weak conclusion given all the words that preceded it. You might as well just start and end with this:
New standards have issues, people don't know how to use them correctly, and compilers are slow to catch up. Wait 5 years to use a standard so all these things are worked out, and you don't have to worry about the details that don't stick.
In any case this is just an example, there are a couple of similar "subsets" with the same idea of keeping C++ use to its bare minimum, it is just that i remember this one because of the name :-P
Thing* thing = new Thing();
thing->DoStuff();
delete thing;
is perfectly valid C++ and has been since the invention of the language. It's not a good idea, but it is valid.If, OTOH, you do
Thing* thing=(Thing*)malloc(sizeof(Thing));
free(thing);
the you fully deserve the inevitable world of pain that is coming your waywe're in 2020.
$ clang-tidy -checks='cppcoreguidelines-*' foo.cpp
/tmp/foo.cpp:10:3: warning: initializing non-owner 'Thing *' with a newly created 'gsl::owner<>' [cppcoreguidelines-owning-memory]
Thing* thing = new Thing();
^
/tmp/foo.cpp:12:3: warning: deleting a pointer through a type that is not marked 'gsl::owner<>'; consider using a smart pointer instead [cppcoreguidelines-owning-memory]
delete thing;
^
/tmp/foo.cpp:10:3: note: variable declared here
Thing* thing = new Thing();
^I'm not arguing it's good C++, but saying "C++'s destructor behaviour doesn't work if you don't use C++" in relation to using new and delete is clearly nonsense.
I guess it depends on your definition of "valid".
Let's say we have:
class Thing
{
public:
Thing() { cout << "created" << endl; }
~Thing() { cout << "destroyed" << endl; }
};
Assuming the happy path, the new/delete version will be functionally identical to a more correct/idiomatic {
auto thing = make_unique<Thing>();
}
The malloc/free version won't call the destructor, and therefore doesn't do what a "normal" object would do.It's splitting hairs, I guess, but that's where I'd draw the line.
RAII naturally composes because a container has to clean up its memory, and it has a well defined lifetime.
Python has a very straightforward model for its context managers, two magic methods:
def __enter__(self):
...
def __exit__(self, etype, eval, tb):
...
To handle many resources being released, the stdlib prefers a more direct approach using a special class: with ExitStack() as stack:
files = [stack.enter_context(open(fname)) for fname in filenames]
And it makes sense. You probably don't want regular containers to have yet another magic method defined on them, and there is no clear single behavior the a container's __enter__ and __exit__ method could have.And it doesn't compose in other ways. In async code, for instance, there's a completely separate mechanism with `async with` that calls __aenter__ and __aexit__.
It really comes down to the fact that when objects have an ambiguous lifetime, which is a consequence of garbage collection, you can't have RAII.