9

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?

Not a real meerkat
  • 5,604
  • 1
  • 24
  • 55
vbar
  • 251
  • 2
  • 8
  • Since C++17 [RVO is *guaranteed* in some circumstances](http://en.cppreference.com/w/cpp/language/copy_elision). – Jesper Juhl Apr 03 '18 at 14:42
  • @JesperJuhl But the book is before C++17. I think the book is wrong. If you pass `-fno-elide-constructors`, the result is certainly wrong. – llllllllll Apr 03 '18 at 14:44
  • Guaranteed RVO applies for *pure rvalues*. At any point your object has *name* , some bets are off. also *prvalues* has a somewhat different meaning in C++17. But there's another twist I'm not sure of – WhiZTiM Apr 03 '18 at 14:47
  • I think Bjarne meant to demonstrate the concept, more than to present a production quality scope guard. Yes, this can be broken, but is showing the full code that fixes it going to help or detract from the point the book is trying to convey in that section? I don't know. – StoryTeller - Unslander Monica Apr 03 '18 at 14:52
  • and for the 10th time today "c++ will never have 'finally' semantics built into the core language because there's no call for it" – pm100 Apr 03 '18 at 18:35

2 Answers2

2

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
Not a real meerkat
  • 5,604
  • 1
  • 24
  • 55
0

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; });
}
Daniel Jour
  • 15,896
  • 2
  • 36
  • 63
  • If you prevent copying Final_action, the instantiation of finally won't compile - e.g. with gcc: `fin.cc:19:41: error: use of deleted function ‘Final_action::Final_action(const Final_action&) [with F = main()::]’ auto act1 = finally([&]{ delete p; }); ^ fin.cc:4:5: note: declared here Final_action(Final_action const &) = delete;` The copy constructor may not (must not) be called, but it must be accessible. – vbar Apr 04 '18 at 16:06
  • @vbar Indeed this seems to only work since C++17 o.O – Daniel Jour Apr 05 '18 at 14:12