Блокировка shared_ptr

У меня есть общий объект, который необходимо отправить в системный API и извлечь его позже. Системный API получает только void *. Я не могу использовать shared_ptr:: get(), потому что он не увеличивает счетчик ссылок и может быть выпущен другими потоками перед извлечением из системного API. Отправка нового shared_ptr * будет работать, но включает в себя дополнительное распределение кучи.

Один из способов сделать это - позволить объекту, полученному из enable_shared_from_this. Однако, поскольку этот шаблон класса имеет только слабый_ptr, этого недостаточно для того, чтобы объект не был выпущен.

Итак, мое решение выглядит следующим образом:

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

Есть ли более простой способ сделать это?

Недостаток этого решения:

  • для макета объекта MyClass требуется дополнительно shared_ptr<MyClass>, кроме weak_ptr в базовом классе enable_shared_from_this.

  • Как я упоминал в комментариях, доступ к lock() и unlock() одновременно НЕ БЕЗОПАСНЫЙ.

  • Хуже всего то, что это решение поддерживает только lock() один раз перед вызовом unlock(). Если один и тот же объект должен использоваться для нескольких вызовов API системы, необходимо выполнить дополнительный подсчет ссылок.

Если у нас есть другой класс enable_lockable_shared_from_this, он будет 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());

И реализация enable_lockable_shared_from_this аналогична enable_shared_from_this, единственное отличие заключается в том, что она реализует lock() и вспомогательную функцию unlock_shared. Вызов этих функций только явно увеличивает и уменьшает use_count(). Это будет идеальным решением, потому что:

  • Это устраняет дополнительную стоимость пространства

  • Повторное использование средств, существующих для shared_ptr, гарантирует безопасность concurrency.

  • Лучшее в этом решении состоит в том, что он поддерживает множество вызовов lock().

Однако, самый большой недостаток: он недоступен на данный момент!

UPDATE:

Как минимум два ответа на этот вопрос включают контейнер указателей. Пожалуйста, сравните эти решения со следующим:

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

Это абсолютно O (1) vs O (n) (пространство, время O (log (n)) или аналогичное, но абсолютно большее, чем O (1)) игра.

Ответ 1

Изменив предыдущий ответ, я, наконец, получаю следующее решение:

//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);
}

Это фактически то же самое, что и моя предыдущая версия. Единственное, что я сделал, это создать более общее решение для непосредственного контроля жизни объекта.

В соответствии со стандартом (5.2.9.13) последовательность static_cast определенно четко определена. Кроме того, поведение "сырой" копии может быть undefined, но мы явно запрашиваем это, поэтому пользователь ДОЛЖЕН проверить совместимость системы перед использованием этого средства.

Кроме того, на самом деле в этом примере требуется только RawCopy(). Ввод ReConstruct предназначен только для общего назначения.

Ответ 2

Теперь у меня есть идея:

template<typename T>
struct locker_helper{
    typedef shared_ptr<T> p_t;
    typedef typename aligned_storage<sizeof(p_t), alignment_of<p_t>::value>::type s_t;
};
template<typename T> void lock_shared(const shared_ptr<T> &p){
    typename locker_helper<T>::s_t value;
    new (&value)shared_ptr<T>(p);
}
template<typename T> void unlock_shared(const shared_ptr<T> &p){
    typename locker_helper<T>::s_t value
        = *reinterpret_cast<const typename locker_helper<T>::s_t *const>(&p);
    reinterpret_cast<shared_ptr<T> *>(&value)->~shared_ptr<T>();
}


template<typename T>
void print_use_count(string s, const shared_ptr<T> &p){
    cout<<s<<p.use_count()<<endl;
}

int main(int argc, char **argv){
    auto pi = make_shared<int>(10);
    auto s = "pi use_count()=";
    print_use_count(s, pi); //pi use_count()=1
    lock_shared(pi);
    print_use_count(s, pi);//pi use_count()=2
    unlock_shared(pi);
    print_use_count(s, pi);//pi use_count()=1
}

а затем мы можем реализовать исходный пример следующим образом:

class MyClass:public enable_shared_from_this { /*...*/ };

/* ... */
auto pobj = make_shared<MyClass>(...);
/* ... */
lock_shared(pobj);
system_api_send_obj(pobj.get());
/* ... */
auto preceived = 
    static_cast<MyClass *>(system_api_reveive_obj())->shared_from_this();
unlock_shared(preceived);

С этой идеей легко реализовать a enable_lockable_shared_from_this. Тем не менее, вышесказанное является более общим, позволяет управлять вещами, которые не получены из класса шаблона enable_lockable_from_this`.

Ответ 3

Почему бы просто не обернуть API void * в то, что отслеживает время жизни ссылок API для вашего объекта?

например.

typedef std::shared_ptr<MyClass> MyPtr;
class APIWrapper
{
    // could have multiple references to the same thing?
    // use multiset instead!
    // are references local to transient messages processed in FIFO order?
    // use a queue!  ... etc. etc.
    std::set<MyPtr, SuitableComparator> references_;

public:
    void send(MyPtr const &ref)
    {
        references_.insert(ref);
        system_api_send_obj(ref.get());
    }
    MyPtr receive(void *api)
    {
        MyPtr ref( static_cast<MyClass *>(api)->shared_from_this() );
        references_.erase(ref);
        return ref;
    }
};

Очевидно (надеюсь), что вы знаете фактическую семантику вашего API, так что выше это просто широкая догадка.

Ответ 4

Как использовать следующие глобальные функции, которые используют указатели-умные-указатели.

template<typename T> void *shared_lock(std::shared_ptr<T> &sp)
{
    return new std::shared_ptr<T>(sp);
}
template<typename T> std::shared_ptr<T> shared_unlock(void *vp)
{
    std::shared_ptr<T> *psp = static_cast<std::shared_ptr<T,D>*>(sp);
    std::shared_ptr<T> res(*psp);
    delete psp;
    return res;
}

У вас есть дополнительный новый/удалить, но исходный тип не изменен. Кроме того, concurrency не проблема, потому что каждый вызов shared_lock возвращает другой shared_ptr.

Чтобы избежать новых/удаленных вызовов, вы можете использовать пул объектов, но я думаю, что это не стоит усилий.

UPDATE

Если вы не собираетесь использовать несколько потоков в этом вопросе, как насчет следующего?

template<typename T> struct SharedLocker
{
    std::vector< std::shared_ptr<T> > m_ptrs;

    unsigned lock(std::shared_ptr<T> &sp)
    {
        for (unsigned i = 0; i < m_ptrs.size(); ++i)
        {
            if (!m_ptrs[i])
            {
                 m_ptrs[i] = sp;
                 return i;
            }
        }
        m_ptrs.push_back(sp);
        return m_ptrs.size() - 1;
    }
    std::shared_ptr<T> unlock(unsigned i)
    {
        return std::move(m_ptrs[i]);
    }
};

Я изменил void* на unsigned, но это не должно быть проблемой. Вы также можете использовать intptr_t, если это необходимо.

Предполагая, что в большинстве случаев в векторе будет всего несколько объектов, возможно даже не более 1, распределения памяти будут происходить только один раз при первой вставке. И в остальное время это будет по нулевой цене.