Earth Engine Earth Engine - 15 days ago 5
C++ Question

Locking a shared_ptr

I have an shared object that need to be send to a system API and extract it back later. The system API receives void * only. I cannot use shared_ptr::get() because it do not increases the reference count and it could be released by other threads before extract from the system API. Sending a newed shared_ptr * will work but involves additional heap allocation.

One way to do it is to let the object derived from enable_shared_from_this. However, because this class template owns only a weak_ptr, it is not enough to keep the object from released.

So my solution looks like the following:

class MyClass:public enable_shared_from_this<MyClass> {
private:
shared_ptr<MyClass> m_this;
public:
void *lock(){
m_this=shared_from_this();
return this;
}
static shared_ptr<MyClass> unlock(void *p){
auto pthis = static_cast<MyClass *>(p);
return move(pthis->m_this);
}
/* ... */
}

/* ... */
autp pobj = make_shared<MyObject>(...);
/* ... */
system_api_send_obj(pobj->lock());
/* ... */
auto punlocked = MyClass::unlock(system_api_reveive_obj());


Are there any easier way to do this?

The downside of this solution:


  • it requires an additional
    shared_ptr<MyClass>
    in the MyClass object layout, in addition of a
    weak_ptr
    in the base class
    enable_shared_from_this
    .

  • As I mentioned in the comments, access to
    lock()
    and
    unlock()
    concurrently is NOT Safe.

  • The worst thing is that this solution can only support
    lock()
    once before a call of
    unlock()
    . If the same object is to be use for multiple system API calls, additional reference counting must be implemented.



If we have another
enable_lockable_shared_from_this
class it will be greate:

class MyClass:public enable_lockable_shared_from_this<MyClass> {
/* ... */
}

/* ... */
autp pobj = make_shared<MyObject>(...);
/* ... */
system_api_send_obj(pobj.lock());
/* ... */
auto punlocked = unlock_shared<MyClass>(system_api_reveive_obj());


And the implementation of
enable_lockable_shared_from_this
is similar as
enable_shared_from_this
, the only difference is it implements
lock()
and a helper function
unlock_shared
. The calling of those functions only explicitly increase and decrease use_count(). This will be the ideal solution because:


  • It eliminate the additional space cost

  • It reuses the facilities existing for shared_ptr to guarantee concurrency safety.

  • The best thing of this solution is that it supports multiple
    lock()
    calls seamlessly.



However, the only biggest downside is: it is not available at the moment!

UPDATE:

At least two answers to this question involves a container of pointers. Please compare those solutions with the following:

class MyClass:public enable_shared_from_this<MyClass> {
private:
shared_ptr<MyClass> m_this;
mutex this_lock; //not necessory for single threading environment
int lock_count;
public:
void *lock(){
lock_guard lck(this_lock); //not necessory for single threading environment
if(!lock_count==0)
m_this=shared_from_this();
return this;
}
static shared_ptr<MyClass> unlock(void *p){
lock_guard lck(this_lock); //not necessory for single threading environment
auto pthis = static_cast<MyClass *>(p);
if(--lock_count>0)
return pthis->m_this;
else {
lock_count=0;
return move(pthis->m_this); //returns nullptr if not previously locked
}
}
/* ... */
}

/* ... */
autp pobj = make_shared<MyObject>(...);
/* ... */
system_api_send_obj(pobj->lock());
/* ... */
auto punlocked = MyClass::unlock(system_api_reveive_obj());


This is absolutely an O(1) vs O(n) (space; time is O(log(n)) or similar, but absolutely greater than O(1) ) game.

Answer

By adjusting the previous answer, I finally get the following solution:

//A wrapper class allowing you to control the object lifetime
//explicitly.
//
template<typename T> class life_manager{
public:
    //Prevent polymorphic types for object slicing issue.
    //To use with polymorphic class, you need to create
    //a container type for storage, and then use that type.
    static_assert(!std::is_polymorphic<T>::value, 
        "Use on polymorphic types is prohibited.");

    //Example for support of single variable constructor
    //Can be extented to support any number of parameters
    //by using varidict template.
    template<typename V> static void ReConstruct(const T &p, V &&v){ 
        new (const_cast<T *>(&p))T(std::forward<V>(v));
    }

    static void RawCopy(T &target, const T &source){
        *internal_cast(&target) = *internal_cast(&source);
    }
private:
    //The standard said that reterinterpret_cast<T *>(p) is the same as 
    //static_cast<T *>(static_cast<void *>(p)) only when T has standard layout.
    //
    //Unfortunately shared_ptr do not.
    static struct { char _unnamed[sizeof(T)]; } *internal_cast(const T *p){
        typedef decltype(internal_cast(nullptr)) raw;
        return static_cast<raw>(static_cast<void *>(const_cast<T *>(p)));
    }
};

//Construct a new instance of shared_ptr will increase the reference
//count. The original pointer was overridden, so its destructor will
//not run, which keeps the increased reference count after the call.
template<typename T> void lock_shared(const std::shared_ptr<T> &p){
    life_manager<shared_ptr<T> >::ReConstruct(p, std::shared_ptr<T>(p));
}

//RawCopy do bit-by-bit copying, bypassing the copy constructor
//so the reference count will not increase. This function copies
//the shared_ptr to a temporary, and so it will be destructed and
//have the reference count decreased after the call.
template<typename T> void unlock_shared(const std::shared_ptr<T> &p){
    life_manager<shared_ptr<T> >::RawCopy(std::shared_ptr<T>(), p);
}

This is actually the same as my previous version, however. The only thing I did is to create a more generic solution to control object lifetime explicitly.

According to the standard(5.2.9.13), the static_cast sequence is definitely well defined. Furthermore, the "raw" copy behavior could be undefined but we are explicitly requesting to do so, so the user MUST check the system compatibility before using this facility.

Furthermore, actually only the RawCopy() are required in this example. The introduce of ReConstruct is just for general purpose.