Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Relying on RVO for finally function

Tags:

c++

c++11

Reading The C++ Programming Language (4th edition), in the Exception Handling chapter, there's an example helper for ad hoc cleanup code:

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);
}

It's used like

auto act1 = finally([&]{ delete p; });

to run the lambda code at the end of the block in which act1 is declared.

I suppose this worked for Stroustrup when he tested it, due to Return Value Optimization limiting Final_action<> to a single instance - but isn't RVO just an optional optimization? If the instance is copied on return from finally, obviously ~Final_action() runs for both copies. In other words, p is deleted twice.

Is such behavior prevented by something in the standard, or is the code just simple enough for most compilers to optimize it?

like image 505
vbar Avatar asked Apr 03 '18 14:04

vbar


1 Answers

Indeed, the example relies on copy ellision, that is only guaranteed (in some circunstances) since C++17.

Having said that, copy ellision is an optimization that is implemented in most modern C++11/C++14 compilers. I'd be surprised if this snippet failed on an optimized build.

If you want to make it bulletproof, though, you could just add a move constructor:

template<typename F>
struct Final_action {
    Final_action(F f): clean{f} {}
    ~Final_action() { if (!moved) clean(); }
    Final_action(Final_action&& o) : clean(std::move(o.clean)) {o.moved = true;}
private:
    F clean;
    bool moved{false};
};

template<class F>
Final_action<F> finally(F f)
{
    return Final_action<F>(f);
}

I don't think that's needed, though. In fact, most compilers do copy ellision even if you don't enable optimizations. gcc, clang, icc and MSVC are all examples of this. This is because copy ellision is explicitly allowed by the standard.

If you add this snippet:

int main() {
    int i=0;
    {
        auto f = finally([&]{++i;});
    }
    return i;
}

and analyze the generated assembly output on godbolt.org, you'll see that Final_action::~Final_action() is generally only called once (on main()). With optimizations enabled, compilers are even more aggressive: check out the output from gcc 4.7.1 with only -O1 enabled:

main:
  mov eax, 1 # since i is incremented only once, the return value is 1.
  ret
like image 187
Cássio Renan Avatar answered Sep 17 '22 12:09

Cássio Renan