Владение общими ресурсами с использованием std::weak_ptr

Мне интересно, как (используя С++ 11 и, надеюсь, с обратными (boost или TR1) совместимыми типами интеллектуальных указателей) достичь:

Один экземпляр класса (ModelController) владеет ресурсом (InputConsumer), а другой компонент (InputSender, который в данном случае является синглтоном) имеет к нему доступ.

Модель InputSender содержит список ссылок на InputConsumers, которых будет много.

ModelController может не иметь ни одного, иметь один или много InputConsumers, и может быть много ModelController. InputSender НЕ знает.

Вот что было бы неплохо: способ для InputSender отслеживать назначенные ему InputConsumers таким образом, чтобы он мог самостоятельно выяснить, действительны ли отдельные InputConsumers или нет.

Мне кажется, что weak_ptr идеально подходят для этой цели, так как их использование требует проверки этого условия.

Если InputSender перестанет отслеживать какой-либо из своих weak_ptr ссылок, ничего страшного не произойдет, соответствующие InputConsumer будут просто находиться в режиме радиомолчания.

Если ModelController удаляется или если ModelController удаляет некоторые из своих InputConsumer, любые InputSender, зарегистрированные с ними, узнают в следующий раз, когда они попытаются получить к ним доступ, что они больше не существуют, и могут очиститься без необходимости отправлять сообщение или сделать что-нибудь.

Итак, вопрос в том, является ли это подходящей ситуацией для использования shared_ptr и weak_ptr? Интересно, полностью ли подходит shared_ptr, потому что InputConsumer концептуально владеют их ModelController, поэтому они должны быть переменными-членами. Я не знаю, насколько разумно для ModelController управлять ими только через shared_ptr. Я не могу сказать, работает ли unique_ptr вместе с weak_ptr. Я должен просто управлять shared_ptrs в ctor/dtor ModelController?

Также может быть хорошо известный (не мне!) шаблон проектирования, в который это попадает, поэтому, если кто-нибудь знает о такой вещи, пожалуйста, сообщите мне.


person Steven Lu    schedule 27.08.2013    source источник


Ответы (2)


У меня нет большого опыта в общих указателях, но да, это кажется очень подходящим использованием weak_ptr.

В данном случае вас просто раздражает, что:

  1. Вы хотели бы использовать InputConsumers непосредственно в качестве членов ModelControllers, так как это тривиальное отношение владения.
  2. Вы вынуждены использовать shared_ptr, чтобы заставить его работать с weak_ptr.

Я думаю, что это решается использованием shared_ptr в качестве алиаса объекта-члена. Согласно C++.com:

Кроме того, объекты shared_ptr могут совместно владеть указателем, в то же время указывая на другой объект. Эта возможность известна как псевдоним (см. конструкторы) и обычно используется для указания на объекты-члены, когда они владеют объектом, которому они принадлежат.

Я никогда не делал этого сам, но это кажется адаптированным к вашей ситуации:

  • Иметь InputConsumers членов ModelControllers
  • Иметь псевдоним shared_ptr для каждого из них
  • Ссылайтесь на них в InputSender, используя weak_ptrs

ИЗМЕНИТЬ

Вот полный минимальный рабочий пример:

#include <iostream>
#include <memory>

using namespace std;

// A class to try our smart pointers on
struct Foo 
{
    Foo() { cout << "constructing Foo\n"; }
    ~Foo() { cout << "destructing Foo\n"; }
};

// A class that owns some Foo as members
struct Owner
{
    // The actual members
    Foo foo1;
    Foo foo2;

    // A fake shared pointer whose purpose is:
    //   1) to be of type shared_ptr<>
    //   2) to have the same lifetime as foo1 and foo2
    shared_ptr<Owner> self;

    // A fake deleter that actually deletes nothing 
    struct Deleter
    {
        void operator() (Owner *) { cout << "pretend to delete Owner\n"; }
    };

    Owner() : self(this, Deleter()) { cout << "constructing Owner\n"; }
    ~Owner()                        { cout << "destructing Owner\n"; }
};

// A class that holds a reference to a Foo
struct Observer
{
    // A reference to a Foo, as a weak pointer
    weak_ptr<Foo> foo_ptr;

    Observer(const shared_ptr<Foo> & foo_ptr) : foo_ptr(foo_ptr)
    {
        cout << "constructing Observer\n";
    }
    ~Observer() { cout << "destructing Observer\n"; }

    void check()
    {
        if(foo_ptr.expired())
            cout << "foo expired\n";
        else
            cout << "foo still exists\n";
    }   
};  

int main()
{
    // Create Owner, and hence foo1 and foo2
    Owner * owner = new Owner;

    // Create an observer, passing an alias of &(owner->foo1) to ctor
    Observer observer(shared_ptr<Foo>(owner->self, &(owner->foo1)));

    // Try to access owner->foo1 from observer
    observer.check();
    delete owner;
    observer.check();

    return 0;
}

Он печатает:

constructing Foo
constructing Foo
constructing Owner
constructing Observer
foo still exists
destructing Owner
pretend to delete Owner
destructing Foo
destructing Foo
foo expired
destructing Observer

Сложность заключается в том, чтобы создать от weak_ptr до owner->foo1 (то же самое будет и для foo2). Для этого нам сначала нужен shared_ptr, который является псевдонимом owner->foo1. Это можно сделать только:

shared_ptr<Foo> alias(other_shared_ptr, &(owner->foo1));

где other_shared_ptr — это shared_ptr<T>, время жизни которого не меньше, чем у owner->foo1. Для этого рекомендуется использовать shared_ptr<T>, который также является членом owner, поскольку это гарантирует, что время жизни будет таким же. Наконец, нам нужен допустимый ненулевой указатель, и, поскольку мы не хотим ничего создавать в куче, мы должны использовать существующий объект. this — хороший кандидат, так как мы знаем, что он действителен и уничтожается только после уничтожения его членов. Следовательно, наш other_shared_ptr, например:

shared_ptr<Owner> self(this);

Однако это означает, что когда self выйдет из области видимости, т.е. во время уничтожения owner, он вызовет delete this. Мы не хотим, чтобы это удаление произошло, иначе this будет удалено дважды (что является неопределённым поведением, на практике это ошибка сегментации). Следовательно, мы также предоставляем конструктору self Deleter, который на самом деле ничего не удаляет.

Остальной код должен быть понятен с комментариями.

person Boris Dalstein    schedule 27.08.2013
comment
Звучит очень круто, но мне не сразу понятно, как их сглаживать! Не могли бы вы опубликовать небольшой пример? Спасибо! - person Steven Lu; 27.08.2013
comment
@StevenLu Я никогда раньше не использовал псевдонимы, поэтому мне пришлось узнать подробности, и на самом деле это сложнее, чем я ожидал. В любом случае, усилия того стоили, теперь я достаточно хорошо их понимаю и имею рабочий пример, которым делюсь с вами (см. редактирование). Альтернативой является не использование этих псевдонимов, а наличие непосредственно shared_ptr<Foo> foo1_ptr членов Owner, инициализированных с помощью Owner() : foo1_ptr(new Foo) {}, а затем непосредственное использование Observer observer(owner->foo1_ptr), но это создает данные в куче, которых вы, возможно, захотите избежать. - person Boris Dalstein; 27.08.2013
comment
Интересно, есть ли способ избежать немного неприличного фальшивого удаления. Какой хаос вызывает игнорирование удаления? Я предполагаю, что это дважды освободит экземпляр класса и приведет к сбою, поскольку shared_ptr предназначен для использования для вновь выделенных элементов. :( Хотя, может быть, я смогу скрыть хитрость, реализовав ее в общем случае в виде базового класса, а может быть, даже лучше шаблона. - person Steven Lu; 28.08.2013
comment
@StevenLu Да, если вы не предоставите этот фиктивный Deleter, owner будет удален дважды, что приведет к segfault (я пытался). Этот хак кажется единственным возможным решением, которое не требует выделения чего-либо в куче. В самом деле, если в куче ничего нет, то это означает, что new не вызывалось, следовательно, delete также не должно вызываться. Но вам нужен shared_ptr для ненулевого адреса (чтобы иметь возможность использовать weak_ptr), и по умолчанию он вызовет удаление, а затем segfault. :-/ Спасибо за комментарий на моем сайте :-) - person Boris Dalstein; 28.08.2013

Просто еще один полный рабочий фрагмент, показывающий динамику std::weak_ptr, возможно, может помочь немного больше. (Мне особенно понравилась эта семантика expired());

#include<iostream>
#include<memory>
#include<string>

class MessageProcessor{                
};

class Message{
public:

        Message(std::shared_ptr<MessageProcessor>  _msg_proc, int _id){
                proc_weak = std::weak_ptr<MessageProcessor>(_msg_proc);
                proc_shared = _msg_proc;
                id = _id;
        }

        std::weak_ptr<MessageProcessor> proc_weak;
        std::shared_ptr<MessageProcessor> proc_shared;        
        int id;
};

int main(){

        std::shared_ptr<MessageProcessor> proc(new MessageProcessor());

        Message msg(proc,1);

        // Here we have proc with 2 shared_ptr refs: 'proc' and 'msg.proc_shared'
        // As expected 'msg.proc_weak is not expired'
        if( !msg.proc_weak.expired() )
                std::cout << "1) proc_weak is not EXPIRED. proc.use_count() == " << proc.use_count() << std::endl;        

        // make one of shared_ptr ref, point to other place
        msg.proc_shared = std::shared_ptr<MessageProcessor>();        

        // there is still the 'proc' reference        
        if( !msg.proc_weak.expired() )
                std::cout << "2) proc_weak is not EXPIRED (yet). proc.use_count() == " << proc.use_count() << std::endl;

        // 'erase' the last reference
        proc = std::shared_ptr<MessageProcessor>(); 

        // Finally... There is no more refs in shared_pointer!
        if( msg.proc_weak.expired() )
                std::cout << "3) proc_weak has EXPIRED. proc.use_count() == " << proc.use_count() << std::endl;

        return 0; 
}
person wesley.mesquita    schedule 30.01.2014