4 ms·
C++11: Try/Catch/Finally Pattern Using RAII & Lambdas
- NerdsCentral 15y agoI was not expecting this to produce so much interest. I guess a lot of people have asked about how to implement try/catch/finally in C++.
- ot 15y agoWhat the OP describes here is a common C++ pattern known as "scope guard" (check "Solution 4" in http://drdobbs.com/184403758 http://drdobbs.com/184403758). It was possible to implement it in C++03 with some tricks but C++11 lambdas make it much easier. This is a very nice small implementation but, as always with C++, devil is in the details: * Creation of an std::function needs a dynamic allocation, so if the allocation fails an exception will be thrown and if the finally is guarding a resource, the resource will be leaked * std::function has a non-negligible calling overhead, hence this should not be used in performance-sensitive code * Checking a condition inside the finally clause is not very elegant, a better idiom in C++ is to support dismissing a scope guard. * The finallyClause may throw an exception, and since it is called in a destructor this is generally considered a bad idea. I don't know what could happen in this case, but some scope guard implementations I've seen catch the exception and explicitly call std::terminate(). I guess this is for performance reasons, because the destructor can be declared nothrow. Here there is a more complex implementation, which addresses most of the corner cases: http://channel9.msdn.com/Shows/Going+Deep/C9-Lectures-Stephan-T-Lavavej-Advanced-STL-6-of-n#c634477472460000000 http://channel9.msdn.com/Shows/Going+Deep/C9-Lectures-Stepha...
- code-dog 15y agoI get your points. In general, the use cases for this pattern are not production code. The post points out this is not a good way of handling resource and that the example is about debugging. It would be up to the implementer to ensure the finally clause does not throw and exception or to catch it (I think there is something about nesting). As for memory allocation failures, my experience is that depends a lot on the platform. It is a common misconception that dynamic memory is likely to run out and therefore we need to worry about that but not automatic memory. However, I have seen stack overflow many many many more times than a genuine 'out of memory'. Further, if an application has actually run out of memory so badly that a allocation of the closure's internal storage barfs then I suspect it cannot be retrieved. Naturally this does not apply if your platform has a very low heap size (e.g. realtime hardware or some such).
- evincarofautumn 15y agoI’ve done this before, and this version has a significant flaw: it is not exception-safe. The destructor should not read as it does: ~finally() { finallyClause(); } This allows exceptions thrown by finallyClause() to propagate out of the destructor, which, if an exception is already being thrown, will result in a call to std::terminate(), causing the application to die horrifically. Because C++ kinda-should’ve-but-really-doesn’t have checked exceptions, we must simply discard them: ~finally() { try { finallyClause(); } catch (...) {} } You can also avoid problems with exceptions and std::function’s dynamic allocation by using a template instead: template<class F> class finally_type { public: explicit finally_type(F f) : function(f) {} ~finally_type() { try { function(); } catch (...) {} } private: finally_type(const finally_type&); finally_type& operator=(const finally_type&); F function; }; template<class F> finally_type<F> finally(F f) { return finally_type<F>(f); } This can be used like so: void test() { int* i = new int; auto cleanup = finally([i]() { delete i; }); may_throw(); } Finally (har!) the equivalent idiom in C++03: void test() { int* i = new int; struct finally { finally(int* i) : i(i) {} ~finally() { delete i; } int* i; } cleanup(i); may_throw(); }
- code-dog 15y agoThat is really nice. Could you expand on how this works a little "You can also avoid problems with exceptions and std::function’s dynamic allocation by using a template instead:"
- evincarofautumn 15y agostd::function can store function pointers (and member function pointers) by value, but using a function-like object that defines operator()—or by extension std::bind()—requires dynamic allocation, which can throw. The templated version, on the other hand, always stores its parameter by value. Come to think of it, you could also make the constructor take an rvalue reference (F&&) to use move construction instead of copy construction.