Search code examples
c++c++11lockingraii

Assignment within RAII scope


Problem

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

Background

  • I have a global lock which can be called with lock() and unlock().

  • I have a type, LockedObject, which can only be initialized when the global lock is locked.

  • I have a function, use_locked(LockedObject &locked_object), which needs to be called with the global lock unlocked.

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.


Solution

  • 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);