0

Lets ask you about this simple scope guard:

template <class T>
struct finop_t {
    T& t;
    ~finop_t() { t(); }
};
#define FINALLY__(l, cl) \
    auto FIN ## l ## clo = cl; \
    finop_t<decltype(FIN ## l ## clo)> FIN ## l ## fin { FIN ## l ## clo}
#define FINALLY_(l, cl) FINALLY__(l, cl)
#define FINALLY(...) FINALLY_(__LINE__, ([=](){__VA_ARGS__}))

int main() {
    FINALLY( std::cout << "hello" << std::endl ; );
    std::cout << "one" << std::endl;
    FINALLY( std::cout << "world" << std::endl ; );
    std::cout << "second" << std::endl;
    return 0;
}

Is it safe to rely on destruction order here? i.e. is it safe assume that ~finop_t() will be called before lambda destructor?

funny_falcon
  • 427
  • 3
  • 11
  • does this compile? You're assigning a temporary to a mutable l-value reference. – Richard Hodges Aug 10 '15 at 17:39
  • It does compile. First, i initialize variable `FIN##l##clo` with a value of closure. (perhaps, copy elision helps here a bit). Then a assign reference to `FIN##l##clo` to field `t` of variable `FIN##l##fin`. – funny_falcon Aug 10 '15 at 17:45
  • ah i see. in that case why not store the object and initialise it directly? Copy elision will ensure no redundant copies and you'll have no indirection through a reference – Richard Hodges Aug 10 '15 at 17:47
  • if you tell me how to do it, I will be very glad. I haven't found simpler way to fight template argument deduction. – funny_falcon Aug 10 '15 at 17:52
  • Well, i've made simple version which rely on copy elision http://stackoverflow.com/questions/31922693/c-why-this-simple-scope-guard-works – funny_falcon Aug 10 '15 at 17:54
  • answer posted as per your request. – Richard Hodges Aug 10 '15 at 17:55

2 Answers2

2

Yes, it is safe. The macro stores a lambda in a local variable. The destruction order for local variables is fixed (in the reverse order of construction). Thus it is guaranteed that ~finop_t() destructor is called before the corresponding lambda (FIN ## l ## clo) destructor.

avsmal
  • 121
  • 2
2

Destruction of local variables takes place in the inverse order of their construction.

And here's a more efficient way, which needs no references and uses copy-elision to construct the lambda in-place.

(note, you may want to consider [&] rather than [=], but that's for you to judge)

#include <iostream>

template <class T>
struct finop_t {
    finop_t(T&& t) : t(std::forward<T>(t)) {}
    T t;
    ~finop_t() { t(); }
};

template<class F>
finop_t<F> make_finop_t(F&& f)
{
    return finop_t<F>(std::forward<F>(f));
}

#define FINALLY__(l, cl) \
auto FIN ## l ## fin = make_finop_t(cl);

#define FINALLY_(l, cl) FINALLY__(l, cl)
#define FINALLY(...) FINALLY_(__LINE__, ([=](){__VA_ARGS__}))

int main() {
    FINALLY( std::cout << "hello" << std::endl ; );
    std::cout << "one" << std::endl;
    FINALLY( std::cout << "world" << std::endl ; );
    std::cout << "second" << std::endl;
    return 0;
}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
  • This is almost the same as my version http://stackoverflow.com/questions/31922693/c-why-this-simple-scope-guard-works But I think it is not good thing to rely on copy elision within destructor (but may be, it is). – funny_falcon Aug 10 '15 at 17:56
  • Oh, thank you for pointing to `[&]` !!! it is really makes a sense. – funny_falcon Aug 10 '15 at 18:00
  • There is no copy elision during a destructor. It takes place during the call to `make_finop_t()` and as RVO (return value optimisation) when returned from `make_finop_t()` - c++11 gives us perfect forwarding all the way. – Richard Hodges Aug 10 '15 at 18:00
  • It's a bad idea to rely on copy elision of finop_t. It is not guaranteed. – avsmal Aug 10 '15 at 18:02
  • @funny_falcon no problem. I like the sneaky technique to make unique variable names. – Richard Hodges Aug 10 '15 at 18:02
  • @avsmal well, the entire c++ standard library pretty much depends on it. While it's not 'guaranteed', compiler publishers are 'strongly advised' to implement it, which means realistically that no-one is going to use a compiler that does not do CE. Even if the compiler chose not to elide the copy, the code will still work - it would simply be discarding a copied-from function pointer. Still more efficient that calling through a reference. – Richard Hodges Aug 10 '15 at 18:05
  • 1
    >> There is no copy elision during a destructor. << But we rely on copy elision in a way that destructor should be called once. Every other ScopeGuard implementation make explicit boolean guard-flag which is cleared on copy, so that they are not rely on copy elision. – funny_falcon Aug 10 '15 at 18:05
  • @funny_falcon but the guard object is never copied. – Richard Hodges Aug 10 '15 at 18:06
  • 1
    it is copied if `-fno-elide-constructors` flag passed to compiller (as @dyp shows in http://melpon.org/wandbox/permlink/B73EuYYKGYFMnJtR ) – funny_falcon Aug 10 '15 at 18:08
  • ah i see. fair enough - i guess put a flag in there and test it (then provide a move constructor). That will all get optimised away anyway. – Richard Hodges Aug 10 '15 at 18:09
  • That is what every one do :) – funny_falcon Aug 10 '15 at 18:11
  • probably because in reality it results in the most efficient code :) – Richard Hodges Aug 10 '15 at 18:12
  • @RichardHodges "the entire c++ standard library pretty much depends on it" - the entire C++ standard library still works correctly in the absence of copy elision. It may be less efficient but still correct. – avsmal Aug 10 '15 at 18:16