简体   繁体   中英

Relying on RVO for finally function

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?

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

This only works since C++17! With C++11 or C++14 it fails because of the deleted copy constructor. Since C++17 there are circumstances which enforce RVO , thus not needing a copy/move constructor.


If the instance is copied [..]

Well, how about disallowing copies to be made?

template<typename F>
struct Final_action {
  Final_action(F f): clean{f} {}
  Final_action(Final_action const &) = delete;
  Final_action & operator=(Final_action const &) = delete;
  ~Final_action() { clean(); }
  F clean;
};

Or derive from boost::noncopyable if you're using boost already.

Further discussion about preventing copies.


#include <iostream>

template<typename F>
struct Final_action {
  Final_action(F f): clean{f} {}
  Final_action(Final_action const &) = delete;
  Final_action & operator=(Final_action const &) = delete;
  ~Final_action() { clean(); }
  F clean;
};

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

int main() {
  auto _ = finally([]() { std::cout << "Bye" << std::endl; });
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM