3 ms·
I’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() { f
by evincarofautumn 15y ago
I’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.
- code-dog 15y ago... because the compiler knows how big the function like object will be at compile time when the template is used but it only knows it at runtime if the class is used. ... I am guessing this is the situation?
- ot 15y ago> Because C++ kinda-should’ve-but-really-doesn’t have checked exceptions, we must simply discard them: This would make the error pass silently, possibly leaving the program in an inconsistent state. It is probably better to call std::terminate() instead, which is what C++ does when an exception is thrown when another exception is active.
- code-dog 15y agoWhich kinda goes to my point about out of memory exceptions - they generally end up being fatal sooner or later. Trying to catch them just results in memory running out somewhere else. The only place you can deal with them is if the code is doing something you expect to cause memory issues because it is consuming an atypically large amount of memory (like a cache or some such) which you can get away with shrinking.
- evincarofautumn 15y agoYou’re quite right that ignoring the (programmer) error is too lax, but std::terminate() is far too extreme. This is perhaps a situation for std::nested_exception: ~finally_type() { try { function(); } catch (...) { std::throw_with_nested(std::logic_error ("Exception thrown from \"finally\".")); } } It might pollute calling code a bit with calls to std::rethrow_if_nested(), but I think it’s worth it for the safety.
- ot 15y agoNo, if the finally clause is called while an exception is active, the runtime will call std::terminate(). This is why it is bad practice to throw exceptions in destructors. Hence, in most cases where the scope guard pattern is used (to guarantee exception safety), your code is basically equivalent to calling std::terminate().
- evincarofautumn 15y agoAlright, yeah, no exception should escape the finally destructor. After so many years of using it, I should know better than to write C++. But anyway, the problem is still programmer error. Just as you shouldn’t throw an exception from a destructor, you shouldn’t throw an exception from a “finally”, because it is, by definition, run in a destructor. So, sure, calling std::terminate() explicitly makes about as much sense as anything, but I’d prefer to ignore (and perhaps log) such erroneous exceptions, for reasons of stability.