Unnamed objects whose lifetime is bound to a block of code?

Viewed 137

The question is pretty simple: On occasion I encounter situations in which I modify some (rather global) state, for example a log level — to preempt bitching about global states: Not my framework, nothing I can do about it ;-).

In order to be nice I should restore the old state after I'm done, so I save it and at the end restore it. This is an obvious case for RAII:

// Some header
/// A RAII class which records a state and restores it upon destruction.
struct StateRestorer
{
    State oldState;
    StateRestorer(State oldStateArg) : oldState(oldStateArg) {}
    ~StateRestorer() { setState(oldState); }
};


// Happens a couple times somewhere in my program
{
    StateRestorer oldStateRestorer(getState());
    State newState(/* whatever */);
    setState(newState);
    // Do actually useful things during the new state
    
    // oldStateRestorer goes out of scope and restores the old state.
}

Now, I don't actually need the oldStateRestorer variable. Don't get me wrong, I do need the object it refers to; but I never access oldStateRestorer in any way. The need to have a name for it is mildly cumbersome. If I were alone I might call it s but I'm a strong proponent of good naming so that people to whom the program is unfamiliar (likely me in two years) can readily understand it. On occasion these state variations are nested so that I have to invent new names lest my compiler warns me that I'm shadowing another variable (which in other cases is a serious warning, and I don't like warnings to begin with and now I'm all upset). As I said, mildly annoying.

The question boils down to this:

Is there a way to have an unnamed object whose lifetime is a block of code in C++?

(If somebody felt compelled to give an example in their favorite different language I wouldn't mind though.)

3 Answers

Is there a way to have an unnamed object with automatic storage duration in C++?

Technically no. However, temporary objects are pretty much similar: They are unnamed and are destroyed automatically. Furthermore, lifetime of a temporary object can be expended by binding a reference to it:

struct silly_example {
    T&& ref;
}

int main()
{
    silly_example has_automatic_storage {
         .ref = T{}; // temporary object
    };
}

In this example, we have a named object with automatic storage which refers to a(n unnamed) temporary object whose lifetime matches that of the automatic object.

I don't think this would be useful for the case that you describe.


Note that this is a typical issue with this kind of RAII types. An example within the standard library is std::lock_guard:

std::mutex some_mutex;

{
    const std::lock_guard<std::mutex>
        must_have_a_name(some_mutex);
    
    // critical section which won't refer to the guard
}

Worst part is not that you must come up with a name, but that const std::lock_guard<std::mutex> (some_mutex); is a valid function decalaration and will compile succesfully without creating the guard.


(If somebody felt compelled to give an example in their favorite different language I wouldn't mind though.)

Python is particularly elegant. Since it doesn't have destructors, it cannot have RAII types like this in the first place.

some_mutex = Lock()

with some_mutex:
    # critical section here

A class that can be used with with uses normal functions (with particular names) rather than constructor and destructor:

class WithExample: 
    def __init__(self, args): 
        pass
      
    def __enter__(self):
        # resource init
        # may return something to be used within the scope
        pass
  
    def __exit__(self): 
        # reource release
        pass

If macros and at least C++17 are on the table, you can get by with the fairly simple:

// Standard issue concatenation macros
#define CONCAT(A, B) CONCAT_(A, B)
#define CONCAT_(A, B) A##B


#define WITH(...) if([[maybe_unused]] auto CONCAT(_dO_nOt_tOuCh, __LINE__)(__VA_ARGS__); true)

The if with init statement, and guaranteed copy elision, is what calls for C++17. The first feature is obvious, but the second is useful since it allows for non-copyable and non-movable types as RAII types.

With that macro in hand, one can simply write

WITH(StateRestorer(getState())) {
  //Your code here.
}

Now, at this point I'm certain the question of multiple RAII objects rises. And one probably thinks we must either do a lot of nesting, or get the warning again if we instead opt to write the rather ugly

WITH(A) WITH(B) WITH(C) {

}

And we can solve it. Either by doing as we did, but using the GNU specific __COUNTER__ macro instead of __LINE__. Or by employing more C++17 to let WITH accepts a comma seperated list of RAII expressions. Under the reasonable assumption that RAII types are always classes, we can do the following

namespace detail {
    template<class... Ts>
    struct glue : Ts... {};

    template<class... Ts>
    glue(Ts...) -> glue<Ts...>;
}
#define WITH(...) if([[maybe_unused]] detail::glue CONCAT(_dO_nOt_tOuCh, __LINE__){__VA_ARGS__}; true)

It uses CTAD to generate a type inheriting from a comma seperated list of RAII types on the spot (and in the very likely case all the expressions are prvalues, initializes the bases directly). With it, we may write

WITH(StateRestorer1(...), StateRestorer2(...)) {
}

The destructors will of course be called in reverse order to the comma seperated list.


As an aside, this annoyance was part of the motivation behind P0577 (Keep That Temporary!). There were some interesting ideas in the paper, but sadly it did not gain traction.

I don't think what you want can be done in C++ directly.

However, if your issue is giving names to objects, you can consider to call a function instead. You don't have to give names to function calls:

#include <iostream>

template <typename RAII,typename...Args>
auto with(Args...args){
    return [=](auto f){
        RAII boring_name{args...};
        f();    
    };
}

struct foo { 
    int state;
    foo(int state) : state(state){}
    ~foo(){ std::cout << "bye " << state << "\n";} 
};

int main() {
   with<foo>(42)(
       [&](){
           std::cout << "hello\n";
           with<foo>(123)(
               [&](){
                   std::cout << "hello nested\n";
               }
           );
       }
   );    
}

Output

hello
hello nested
bye 123
bye 42
Related