Is there a C++ standard class to set a variable to a value at scope exit
Asked Answered
W

4

21

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?

Wispy answered 15/4, 2016 at 10:10 Comment(11)
Interesting idea. Something I wonder: is there a reason to use a pointer vs a reference here?Groundwork
I think there is no such thing in the standard library. Andrei Alexandrescu once built some generalization of the above with "lengthy" macro hacks.Hadst
@Groundwork No. You can implement it with a reference.Wispy
Also there is Boost.ScopeExit.Hadst
"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?Corina
@BaummitAugen: The concept of ScopeGuards 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.Rubino
@Rubino 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: "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.Hadst
This construct will be absolutely deadly if the object is accessed from multiple threads.Buchheim
@PeteBecker Equally deadly as var = false; ...Lactometer
@Lactometer - not quite, since the code that resets the variable would be more-or-less invisible.Buchheim
There is a proposal to introduce such a scope_guard feature in the standard by Peter Sommerlad: open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3949.pdfWispy
M
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;

Masorete answered 15/4, 2016 at 10:20 Comment(3)
I'll add further notes and exception specifications as soon as I get a chance.Masorete
(Peter Sommerlad's paper that you're referring to has a new revision: P0052r1.)Anew
@KerrekSB. I'm glad that proposal hasn't fallen by the way side.Masorete
P
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.

Photoelectron answered 15/4, 2016 at 10:29 Comment(10)
I did not downvote, but probably it's not guaranteed that the deleter will execute when the first constructor argument is nullAngieangil
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.Photoelectron
Dear downvoter, if the reason was std::nullptr, it's corrected now. Silly mistake, sorry for that.Photoelectron
Thanks, now I learned a new reason why using namespace std is bad ;-)Photoelectron
@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).Masorete
@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".Masorete
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.)Photoelectron
@Lactometer I don't follow. The answer initialises the shared_ptr with nullptr, not the address (not pointer) of any variable.Masorete
@Masorete 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.Lactometer
@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.Tut
E
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.

Enallage answered 15/4, 2016 at 10:33 Comment(0)
L
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.

Lactometer answered 18/4, 2016 at 11:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.