Last Update:
final_act - follow-up
Table of Contents
Last time I wrote about
final_act
utility, and it seems I’ve stepped into a bigger area that I
wasn’t aware of. Let’s continue with the topic and try to understand
some of the problems that were mentioned in the comments.
Intro
Let’s remind what was the case last time:
I want to call a custom cleanup code at the end of the scope, and I want to be sure it’s invoked.
bool Scanner::scanNodes()
{
// code...
addExtraNodes();
auto _ = finally([] { removeExtraNodes(); });
// code...
return true;
}
I’ve used finally()
from GSL that internally works on final_act
object.
The most important thing!
OK, I know… I made a typo in the title of my original post! :)
I tried it several times, sent newsletter with the proper name… but the
post was wrong :)
GSL -> Guideline Support Library, not GLS -> Guideline Library Support
Important use case
Last time I forgot to mention one huge case where all of those
scope_exit
/final_act
stuff might be utilized.
I mean: transactions. That’s a general term for all of the actions that should be reverted when something fails. If you copied 95% of a file and got an error, you cannot leave such possibly corrupted file; you have to remove it and maybe start again. If you connected to a database and you want to write some records, you assume it’s atomic. I think this idea was ‘hidden’ somewhere in my examples, but it should be more exposed.
So whenever you’re dealing with code that has to be atomic, and transactional, such code constructs might be helpful. Sometimes you can wrap it in a RAII; often explicit code needs to be used.
No exceptions
First of all, my initial assumption was to use final_act
in an
environment where there are not many exceptions. For example, a lot of
legacy code doesn’t use exceptions. Also Google C++ coding guideline
doesn’t prefer exceptions (for practical reasons). This is a strong
assumption, I know, maybe I did this automatically :)
Without exception handling around, we need to take care only of early
returns. In that context, final_act
works as expected.
With exceptions
OK… so what are the problems with exceptions then? final_act
will work
in most cases, so don’t just drop it whenever you have a code with
exceptions… but we need to carefully look at some delicate parts here.
First thing: final act is noexcept
As explained many times through the comments in GSL repo (for example here), other issues
And from Final_act can lead to program termination if the final act throws an exception:
Final_act should be noexcept. It is conceptually just a handy way for the user to conjure up a destructor, and destructors should be noexcept. If something it invokes happens to throw, then the program will terminate.
In other words you should write the code that will be called with the same assumptions as other destructor code… so don’t throw anything there. That might be a little limitation when you want to call some ‘normal’ code, not just some clean-up stuff (on the other hand might that would be a bad design after-all?).
I’ve just notices a really great explanation why destructors shoudn’t throws:
from isocpp.org/faq
Write a message to a log-file. Terminate the process. Or call Aunt Tilda. But do not throw an exception!
Throwing from ctor or copy ctor
There’s a long-standing bug in the current implementation:
How to workaround the bug?
We’re looking at this code:
explicit final_act(F f) noexcept
: f_(std::move(f))
, invoke_(true)
{
}
final_act(final_act&& other) noexcept
: f_(std::move(other.f_))
, invoke_(other.invoke_)
{
other.invoke_ = false;
}
And especially those f_(std::move(other.f_))
calls.
The problem will occur if we raise an exception from the move/copy constructor. As I see this, it can happen only with custom move code that we have for the callable object. We should be safe when we use only lambdas as in:
auto _ = finally([] { removeExtraNodes(); });
Since lambdas (update: with no params) will have default code that won’t throw.
So maybe it’s not a major limitation?
update: I missed one thing. Take look at the example provided in the comment at r/cpp. An exception can also be thrown from a copy/move constructor from some argument of the lambda object (since lambdas are ‘internally’ represented as functor objects and their params are members of that functor). Still, this is probably a quite rare case.
Still, if you plan to use some advanced/custom callable functors, with
special move code then it might be good to take something different than
final_act
.
Other solutions
To be honest, I also assumed that since final_act
is proposed in Core
Guidelines, then it’s the best choice that we have in Modern C++! But
apparently we have some other possibilities:
The talk
First of all please watch this:
CppCon 2015: Andrei Alexandrescu “Declarative Control Flow”
The paper
And read that:
PDF, P0052R3 - Generic Scope Guard and RAII Wrapper for the Standard Library
Roughly, the plan is to have (C++20?) a set of tools:
std::scope_exit
std::scope_success
std::scope_fail
scope_exit
is meant to be a general-purpose scope guard that calls its exit function when a scope is exited. The class templatesscope_fail
andscope_success
share thescope_exit
’s interface, only the situation when the exit function is called differs. These latter two class templates memorize the value ofuncaught_exceptions()
on construction and in the case ofscope_fail
call the exit function on destruction, thenuncaught_exceptions()
at that time returns a greater value, in the case ofscope_success
whenuncaught_exceptions()
on destruction returns the same or a lesser value.
this assumes uncaught_exceptions()
returns int not just bool.
folly/ScopeGuard.h
There’s already working code
D Language
In D we have built-in support for such structures:
scope(exit) removeExtraNodes();
see here for some examples Dlang: Exception Safety
Copy elision
The existing code works now and doesn’t rely on Guaranteed Copy Elision
that we’ll have in C++17. In order to support this they have to
introduce that special bool
parameter.
See discussion in Final_act copy/move semantics is wrong
Summary
As it appears final_act
is a simple utility that should work well in
case where your exit code doesn’t throw exceptions (and also doesn’t
throw from copy/move constructors!). Still, if you need some more
advanced solutions you might want to wait for general
std::scope_exit/_success/_fail
utilities.
One of the most important use case is whenever we need transactional approach with some actions. When we require to call some clean-up code after it succeeded or failed.
Meta-blogging-opinion: The beauty of blogging is that often you write about one topic and you unravel (for yourself) a whole new areas. That way blogging is a great way of learning things!
BTW: as a homework you can write a macro FINALLY
that wraps the
creation of the auto variable and makes sure we have a different name
for that variable - so that you might have several final blocks in a
function/scope.
I've prepared a valuable bonus if you're interested in Modern C++!
Learn all major features of recent C++ Standards!
Check it out here: