Reputation: 723
Here's a code from Stroustrup's "The C++ Programming Language" that implements a finally
which I cannot quiet understand where the destructor gets called.
template<typename F> struct Final_action
{
Final_action(F f): clean{f} {}
~Final_action() { clean(); }
F clean;
}
template<class F>
Final_action<F> finally(F f)
{
return Final_action<F>(f);
}
void test(){
int* p=new int{7};
auto act1 = finally( [&]{delete p;cout<<"Goodbye,cruel world\n";} );
}
I have two questions around this:
According to the author, the delete p
only gets called once: when act1 goes out of scope. But from my understanding: first, act1
will be initialized with the copy constructor, then the temporary object Final_action<F>(f)
in the function finally
gets destructed, calling delete p
for the first time, then a second time at the end of the function test
when act1
is out of scope. Where am I getting it wrong?
Why is the finally
function needed? Can't I just define Final_action act1([&]{delete p;cout<<"Goodbye,cruel world\n"})
? Is that the same?
Also, if anyone can think of a better title, please modify the current one.
UPDATE: After some further thinking, I'm now convinced that the destructor may be called thrice. The additional one is for the temporary objected auto-generated in the calling function void test()
used as the argument to the copy constructor of act1
. This can be verified with -fno-elide-constructors
option in g++. For those who have the same question as me, see Copy elision as well as Return value optimization as pointed out in the answer by Bill Lynch.
Upvotes: 16
Views: 1517
Reputation: 137310
Simplest fix is
template<typename F>
struct Final_action
{
Final_action(F f): clean{std::move(f)} {}
Final_action(const Final_action&) = delete;
void operator=(const Final_action&) = delete;
~Final_action() { clean(); }
F clean;
};
template<class F>
Final_action<F> finally(F f)
{
return { std::move(f) };
}
And use as
auto&& act1 = finally( [&]{delete p;cout<<"Goodbye,cruel world\n";} );
The use of copy-list-initialization and a lifetime-extending forwarding reference avoid any copy/moving of the Final_action
object. Copy-list-initialization constructs the temporary Final_action
return value directly, and the temporary returned by finally
has its lifetime extended by binding to act1
- also without any copying or moving.
Upvotes: 5
Reputation: 6666
The code is broken. The revised code mentioned by SimonKraemer is also broken – it doesn't compile (the return statement in finally
is illegal because Final_action
is neither copyable nor moveable). Making Final_action
move-only with generated move constructor also doesn't work because is F
guaranteed to have a move constructor (if it hasn't, then Final_action
's generated move constructor will silently use F
's copy constructor as a fallback), nor is F
guaranteed to be a no-op after the move. In fact, the lambda from the example will not turn into a no-op.
There is a relatively easy and portable solution:
Add a flag bool valid = true;
to Final_action
and overwrite move c'tor and move assignment to clear the flag in the source object. Only invoke clean()
if valid
. This prevents the generation of copy c'tor and copy assignment, so they do not need to be explicitly deleted. (Bonus points: Put the flag into a reusable move-only wrapper so that you don't have to implement the move c'tor and move assignment of Final_action
. You don't need the explicit deletes in this case either.)
Alternatively, remove Final_action
's template argument and change it to use std::function<void()>
instead. Check that clean
is not empty before invoking it. Add move c'tor and move assignment that set the original std::function
to nullptr
. (Yes, this is necessary to be portable. Moving a std::function
does not guarantee that the source will be empty.) Advantage: The usual benefits of type erasure, such as being able to return the scope guard into an outer stack frame without exposing F
. Disadvantage: may add significant run time overhead.
In my current work project, I basically combined the two approaches with a ScopeGuard<F>
and an AnyScopeGuard
using a type erasing function object. The former uses a boost::optional<F>
and can be converted to the latter. As an added benefit of allowing scope guards to be empty, I can explicitly dismiss()
them as well. This allows using the scope guard to set up the rollback part of a transaction and then dismissing it upon commit (with non-throwing code).
UPDATE: Stroustrup's new example doesn't even compile. I missed that explicitly deleting the copy c'tor also disables the generation of the move c'tor.
Upvotes: 3
Reputation: 81916
You are correct, this code is broken. It only works correctly when return value optimizations are applied. This line:
auto act1 = finally([&]{delete p;cout<<"Goodbye,cruel world\n"})
May or may not invoke the copy constructor. If it does, then you will have two objects of type Final_action
and you will thus call that lambda twice.
Upvotes: 13