Veedrac
Veedrac

Reputation: 60197

Assignment within RAII scope

Problem

How do you initialize an object inside a RAII scope, and use it outside of that scope?

Background

The usage scenario is

lock();
LockedObject locked_object;
unlock();
use_locked(locked_object);

RAII

For various reasons, I moved to a RAII encapsulation of the global lock. I would like to use this everywhere, primarily as creating LockedObject can fail with exceptions.

The problem is that

{
    GlobalLock global_lock;
    LockedObject locked_object;
}
use_locked(locked_object);

fails, as locked_object is created in the inner scope.

Examples

Set-up (mostly not important):

#include <assert.h> 
#include <iostream>

bool locked = false;

void lock() {
    assert(!locked);
    locked = true;  
}

void unlock() {
    assert(locked);
    locked = false;
}

class LockedObject {
    public:
        LockedObject(int i) {
            assert(locked);
            std::cout << "Initialized: " << i << std::endl;
        }
};

void use_locked(LockedObject locked_object) {
    assert(!locked);
}

class GlobalLock {
    public:
        GlobalLock() {
            lock();
        }

        ~GlobalLock() {
            unlock();
        }
};

Original, non RAII method:

void manual() {
    lock();
    LockedObject locked_object(123);
    unlock();
    use_locked(locked_object);
}

Broken RAII methods:

/*
void raii_broken_scoping() {
    {
        GlobalLock global_lock;

        // Initialized in the wrong scope
        LockedObject locked_object(123);
    }
    use_locked(locked_object);
}
*/

/*
void raii_broken_initialization() {
    // No empty initialization
    // Alternatively, empty initialization requires lock
    LockedObject locked_object;
    {
        GlobalLock global_lock;
        locked_object = LockedObject(123);
    }
    use_locked(locked_object);
}
*/

And a main function:

int main(int, char **) {
    manual();
    // raii_broken_scoping();
    // raii_broken_initialization;
}

For what it's worth, in Python I would do:

with GlobalLock():
    locked_object = LockedObject(123)

I want the equivalent of that. I mention my current solution in an answer, but it feels clumsy.


The specific (but simplified) code to be executed follows. With my current lambda-based call:

boost::python::api::object wrapped_object = [&c_object] () {
    GIL lock_gil;
    return boost::python::api::object(boost::ref(c_object));
} ();

auto thread = std::thread(use_wrapped_object, c_object);

with

class GIL {
    public:
        GIL();
        ~GIL();

    private:
        GIL(const GIL&);
        PyGILState_STATE gilstate;
};
GIL::GIL() {
    gilstate = PyGILState_Ensure();
}

GIL::~GIL() {
    PyGILState_Release(gilstate);
}

boost::python::api::objects must be created with the GIL and the thread must be created without the GIL. The PyGILState struct and function calls are all given to me by CPython's C API, so I can only wrap them.

Upvotes: 1

Views: 606

Answers (3)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275800

Here is a complete list of options from my perspective. optional would be what I would do:

The proposed post-C++1y optional would solve your problem, as it lets you construct data after declaration, as would heap based unique_ptr solutions. Roll your own, or steal ot from boost

A 'run at end of scope' RAII function storer (with 'commit') can also make this code less crazy, as can letting your locks be manually disengaged within their scope.

template<class F>
struct run_at_end_of_scope {
  F f;
  bool Skip;
  void commit(){ if (!Skip) f(); Skip = true; }
  void skip() { Skip = true; }
  ~run_at_end_of_scope(){commit();}
};
template<class F>
run_at_end_of_scope<F> at_end(F&&f){ return {std::forward<F>(f), false}; }

then:

auto later = at_end([&]{ /*code*/ });

and you can later.commit(); or later.skip(); to run the code earlier or skip running it.

Making your RAII locking classes have move constructors would let you do construction in another scope, and return via move (possibly elided).

LockedObject make_LockedObject(){
  GlobalLock lock;
  return {};
}

Upvotes: 2

vz0
vz0

Reputation: 32923

Allocate your object on the heap and use some pointers:

std::unique_ptr<LockedObject> locked_object;
{
    GlobalLock global_lock;
    locked_object.reset(new LockedObject());
}
use_locked(locked_object);

Upvotes: 3

Veedrac
Veedrac

Reputation: 60197

My current solution is to use an anonymous function:

void raii_return() {
    LockedObject locked_object = [&] () {
        GlobalLock global_lock;
        return LockedObject(123);
    } ();
    use_locked(locked_object);
}

The advantage of this approach is that it avoids pointers and thanks to copy elision it should be quite fast.

One downside is that LockedObjects don't necessarily support copying (use_locked would in that case take a reference).

Upvotes: 0

Related Questions