20

Within the scope of a member function, I want to temporarly set a member variable to a certain value.

Then, when this function returns, I want to reset this member variable to a given known value.

To bo safe against exceptions and multiple returns, and I've done it with a simple RAII like class. It's defined within the scope of the member function.

void MyClass::MyMemberFunction() {
    struct SetBackToFalse {
        SetBackToFalse(bool* p): m_p(p) {}
        ~SetBackToFalse() {*m_p=false;}
    private:
        bool* m_p;
    };

    m_theVariableToChange = true;
    SetBackToFalse resetFalse( &m_theVariableToChange ); // Will reset the variable to false.

    // Function body that may throw.
}

It seems so obviously commonplace, that I was wondering if there was any such template class doing this in the C++ standard library?

Didier Trosset
  • 36,376
  • 13
  • 83
  • 122
  • 1
    Interesting idea. Something I wonder: is there a reason to use a pointer vs a reference here? – underscore_d Apr 15 '16 at 10:17
  • I think there is no such thing in the standard library. Andrei Alexandrescu once built some generalization of the above with "lengthy" macro hacks. – Baum mit Augen Apr 15 '16 at 10:19
  • @underscore_d No. You can implement it with a reference. – Didier Trosset Apr 15 '16 at 10:20
  • 2
    Also there is [Boost.ScopeExit](http://www.boost.org/doc/libs/1_60_0/libs/scope_exit/doc/html/index.html). – Baum mit Augen Apr 15 '16 at 10:21
  • _"It seems so obviously commonplace"_ It seems more obviously commonplace _not_ to do something so confusing and weird. Why don't you use a different variable instead? – Lightness Races in Orbit Apr 15 '16 at 10:26
  • 2
    @BaummitAugen: The concept of [ScopeGuards](http://www.drdobbs.com/cpp/generic-change-the-way-you-write-excepti/184403758) was really invented by Petru Marginean, rather than Andrei Alexandresu. Couldn't find an authoritative reference, so I'll have to take [Herb Sutter's word for it](https://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/). – IInspectable Apr 15 '16 at 11:58
  • 2
    @IInspectable I don't know who invented it, it's just the only implementation I know. But thx for the links. Also, from the [standards proposas](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189.pdf): *"This proposal incorporates what Andrej Alexandrescu described as scope guard long ago and explained again at C++ Now 2012 ()."* Iirc that's the talk I'm referring to above. – Baum mit Augen Apr 15 '16 at 12:12
  • 3
    This construct will be absolutely deadly if the object is accessed from multiple threads. – Pete Becker Apr 15 '16 at 12:17
  • 2
    @PeteBecker Equally deadly as `var = false;` ... – M.M Apr 18 '16 at 10:35
  • 1
    @M.M - not quite, since the code that resets the variable would be more-or-less invisible. – Pete Becker Apr 18 '16 at 12:25
  • There is a proposal to introduce such a scope_guard feature in the standard by Peter Sommerlad: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3949.pdf – Didier Trosset Apr 22 '16 at 21:56

4 Answers4

11

Not yet (there have been proposals for this). But implementing a generic one is simple enough;

struct scope_exit {
  std::function<void()> f_;
  explicit scope_exit(std::function<void()> f) noexcept : f_(std::move(f)) {}
  ~scope_exit() { if (f_) f_(); }
};
// ...
m_theVariableToChange = true;
scope_exit resetFalse([&m_theVariableToChange]() { m_theVariableToChange = false; });

For simplicity above, I've redacted the copy and move constructors etc...

Marking them as = delete will make the above a minimal solution. Further; moving could be allowed if desired, but copying should be prohibited.


A more complete scope_exit would look like (online demo here);

template <typename F>
struct scope_exit {
  F f_;
  bool run_;
  explicit scope_exit(F f) noexcept : f_(std::move(f)), run_(true) {}
  scope_exit(scope_exit&& rhs) noexcept : f_((rhs.run_ = false, std::move(rhs.f_))), run_(true) {}
  ~scope_exit()
  {
    if (run_)
      f_(); // RAII semantics apply, expected not to throw
  }

  // "in place" construction expected, no default ctor provided either
  // also unclear what should be done with the old functor, should it
  // be called since it is no longer needed, or not since *this is not
  // going out of scope just yet...
  scope_exit& operator=(scope_exit&& rhs) = delete;
  // to be explicit...
  scope_exit(scope_exit const&) = delete;
  scope_exit& operator=(scope_exit const&) = delete;
};

template <typename F>
scope_exit<F> make_scope_exit(F&& f) noexcept
{
  return scope_exit<F>{ std::forward<F>(f) };
}

Notes on the implementation;

  • std::function<void()> can be used to erase the type of the functor. std::function<void()> offers exception guarantees on the move constructors based on the exception specific of the held function. A sample of this implementation is found here
  • These exception specifications are consistent the C++ proposal and GSL implementations
  • I've redacted most of the motivation for the noexcept, more substantial detail is found in the C++ proposal
  • The "usual" RAII semantics of the destructor, hence the "scope exit" function is applicable; it will not throw, this is also consistent with the C++11 specification on the default exception specification for a destructor. See cppreference, SO Q&A, GotW#47 and HIC++

Other implementations can be found;

Community
  • 1
  • 1
Niall
  • 30,036
  • 10
  • 99
  • 142
  • I'll add further notes and exception specifications as soon as I get a chance. – Niall Apr 16 '16 at 10:25
  • 2
    (Peter Sommerlad's paper that you're referring to has a new revision: [P0052r1](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0052r1.pdf).) – Kerrek SB Apr 16 '16 at 10:33
  • @KerrekSB. I'm glad that proposal hasn't fallen by the way side. – Niall Apr 18 '16 at 08:59
4

You could 'abuse' shared_ptr for this:

m_theVariableToChange = true;
std::shared_ptr<void> resetFalse(nullptr, [&](void*){ m_theVariableToChange = false; });

If there are concerns about using void as template parameter T, I found the following in the C++ standard:

20.8.2.2§2:

... The template parameter T of shared_ptr may be an incomplete type.

This indicates that T is only used as a pointer, therefore using void should be fine.

alain
  • 11,939
  • 2
  • 31
  • 51
  • 1
    I did not downvote, but probably it's not guaranteed that the deleter will execute when the first constructor argument is null – Viktor Sehr Apr 15 '16 at 11:26
  • Yes, that could be a reason, thanks @Viktor. However, I tested it, it seems to work. Another reason could be using `void` I guess. I'm not sure if this is legal. – alain Apr 15 '16 at 11:33
  • Dear downvoter, if the reason was `std::nullptr`, it's corrected now. Silly mistake, sorry for that. – alain Apr 15 '16 at 12:06
  • Thanks, now I learned a new reason why `using namespace std` is bad ;-) – alain Apr 15 '16 at 12:23
  • @ViktorSehr. Given `~shared_ptr()` effects "If *this is empty or shares ownership with another shared_ptr instance (use_count() > 1), there are no side effects." and "A shared_ptr object is empty if it does not own a pointer." §20.10.2.2, it should be ok for constructing it with a null pointer - it then owns a null pointer; but I think it is only just ok, I'm sure if it was originally intended to work this way (but it does seem to work). – Niall Apr 15 '16 at 13:18
  • @alain. It works - but must be used carefully. The semantics of the `shared_ptr` allow for shared ownership and if the `shared_ptr` is inadvertently copied, the "execute at scope exit" may not work as expected. The onus would be on the developer to make sure there are no copies made - the compiler would not be able to help in this regard. As an aside; a `unique_ptr` would not work here, since it's destructor requires a non-null pointer before it runs the "deleter". – Niall Apr 15 '16 at 13:23
  • Thanks for the info @Niall. A copy to another `shared_ptr` with the same scope would be ok though. (I guess that would be the most common case of inadvertently copying it.) – alain Apr 15 '16 at 13:34
  • @M.M I don't follow. The answer initialises the `shared_ptr` with `nullptr`, not the address (not pointer) of any variable. – Niall Apr 18 '16 at 11:25
  • @Niall OK, I was imagining that the `unique_ptr` would point to the variable in question, and the custom deleter would set it to false instead of invoking `delete`. But that is a lame idea as it is unintuitive to a reader. – M.M Apr 18 '16 at 11:38
  • @Niall, it's no accident that it works, that's a feature of shared_ptr. The relevant constructor very clearly says it owns the pointer. – Jonathan Wakely Apr 18 '16 at 11:47
1

There is no standard version of this.

The CppGoreGuidelines Support Library (GSL) has a generalized version of this called finally but that library is not production quality yet. Its definitely recommended practice.

E.19: Use a final_action object to express cleanup if no suitable resource handle is available

Reason

finally is less verbose and harder to get wrong than try/catch.

Example

void f(int n)
{
    void* p = malloc(1, n);
    auto _ = finally([p] { free(p); });
    // ...
}

Note

finally is not as messy as try/catch, but it is still ad-hoc. Prefer proper resource management objects.

Galik
  • 47,303
  • 4
  • 80
  • 117
1

Similar question: The simplest and neatest c++11 ScopeGuard

On that thread is described a similar guard for invoking an arbitrary function. To solve your problem, invoke a lambda that resets your variable.

For example, the solution from this answer for your code would be:

scope_guard guard1 = [&]{ m_theVariableToChange = false; };

Another answer on that thread notes that a similar concept has been proposed for C++17 standardization; and there is also a C++03 solution presented.

Community
  • 1
  • 1
M.M
  • 138,810
  • 21
  • 208
  • 365