Какая польза от `enable_shared_from_this`?

Я наткнулся на enable_shared_from_this, читая примеры Boost.Asio, и после прочтения документации я все еще не понимаю, как это следует правильно использовать. Может ли кто-нибудь дать мне пример и объяснение того, когда использование этого класса имеет смысл.


person fido    schedule 03.04.2009    source источник


Ответы (6)


Это позволяет вам получить действительный экземпляр shared_ptr для this, когда все, что у вас есть, это this. Без него у вас не было бы возможности получить от shared_ptr до this, если у вас уже не было такового в качестве участника. Этот пример из документации по расширению для enable_shared_from_this:

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_from_this();
    }
}

int main()
{
    shared_ptr<Y> p(new Y);
    shared_ptr<Y> q = p->f();
    assert(p == q);
    assert(!(p < q || q < p)); // p and q must share ownership
}

Метод f() возвращает действительный shared_ptr, даже если у него не было экземпляра члена. Обратите внимание, что вы не можете просто сделать это:

class Y: public enable_shared_from_this<Y>
{
public:

    shared_ptr<Y> f()
    {
        return shared_ptr<Y>(this);
    }
}

Общий указатель, который возвращается, будет иметь счетчик ссылок, отличный от «правильного», и один из них в конечном итоге потеряет и удержит висящую ссылку при удалении объекта.

enable_shared_from_this стал частью стандарта C ++ 11. Вы также можете получить его оттуда или от буста.

person 1800 INFORMATION    schedule 03.04.2009
comment
+1. Ключевым моментом является то, что очевидный метод простого возврата shared_ptr ‹Y› (this) не работает, потому что это приводит к созданию нескольких отдельных объектов shared_ptr с отдельными счетчиками ссылок. По этой причине вы никогда не должны создавать более одного shared_ptr из одного необработанного указателя. - person j_random_hacker; 03.04.2009
comment
Следует отметить, что в C ++ 11 и более поздних версиях совершенно допустимо использовать конструктор std::shared_ptr в необработанном указателе если наследуется от std::enable_shared_from_this. Я не знаю, была ли обновлена ​​семантика Boost для поддержки этого. - person Matthew; 10.10.2017
comment
@MatthewHolder У вас есть цитата по этому поводу? На cppreference.com я прочитал Создание std::shared_ptr для объекта, которым уже управляет другой std::shared_ptr, не будет обращаться к внутренней сохраненной слабой ссылке и, таким образом, приведет к неопределенному поведению. (en.cppreference.com/w/cpp/memory/enable_shared_from_this) - person Thorbjørn Lindeijer; 01.05.2018
comment
Почему ты не можешь просто сделать shared_ptr<Y> q = p? - person Dan M.; 08.05.2018
comment
@DanM. AFAIK вы можете. Я не понимаю, что вариант использования увядает - person HankTheTank; 02.01.2019
comment
@DanM. Можно, поэтому этот образец не очень полезен. Тем не менее, для него определенно есть варианты использования. Когда нет q и вам нужен p изнутри класса. - person Hatted Rooster; 05.01.2019
comment
@DanM. да, вы можете это сделать, потому что p и q оба shared_ptr. enable_shared_from_this требуется, когда вы находитесь внутри класса, например в методе f (), который у меня был выше. Это упрощенный пример, но он показывает самое главное. - person 1800 INFORMATION; 21.01.2019
comment
@ ThorbjørnLindeijer, вы правы, это C ++ 17 и новее. Некоторые реализации следовали семантике C ++ 16 до того, как она была выпущена. Правильная обработка C ++ 11 - C ++ 14 должна заключаться в использовании std::make_shared<T>. - person Matthew; 07.03.2019
comment
Так это в основном то же самое, что и boost :: intrusive_ptr? В чем разница ? - person jcxz; 26.07.2019

из статьи доктора Доббса о слабых указателях, я думаю, что этот пример легче понять (источник: http://drdobbs.com/cpp/184402026):

... такой код не будет работать правильно:

int *ip = new int;
shared_ptr<int> sp1(ip);
shared_ptr<int> sp2(ip);

Ни один из двух shared_ptr объектов не знает о другом, поэтому оба попытаются освободить ресурс, когда они будут уничтожены. Обычно это приводит к проблемам.

Точно так же, если функции-члену требуется объект shared_ptr, владеющий объектом, для которого она вызывается, она не может просто создать объект на лету:

struct S
{
  shared_ptr<S> dangerous()
  {
     return shared_ptr<S>(this);   // don't do this!
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->dangerous();
   return 0;
}

Этот код имеет ту же проблему, что и предыдущий пример, но в более тонкой форме. Когда он построен, объект shared_ptr sp1 владеет вновь выделенным ресурсом. Код внутри функции-члена S::dangerous не знает об этом объекте shared_ptr, поэтому объект shared_ptr, который он возвращает, отличается от sp1. Копирование нового объекта shared_ptr в sp2 не помогает; когда sp2 выходит за пределы области видимости, он освобождает ресурс, а когда sp1 выходит за пределы области действия, он снова освобождает ресурс.

Способ избежать этой проблемы - использовать шаблон класса enable_shared_from_this. Шаблон принимает один аргумент типа шаблона, который является именем класса, определяющего управляемый ресурс. Этот класс, в свою очередь, должен быть унаследован от шаблона публично; нравится:

struct S : enable_shared_from_this<S>
{
  shared_ptr<S> not_dangerous()
  {
    return shared_from_this();
  }
};

int main()
{
   shared_ptr<S> sp1(new S);
   shared_ptr<S> sp2 = sp1->not_dangerous();
   return 0;
}

При этом помните, что объект, для которого вы вызываете shared_from_this, должен принадлежать объекту shared_ptr. Это не сработает:

int main()
{
   S *p = new S;
   shared_ptr<S> sp2 = p->not_dangerous();     // don't do this
}
person Artashes Aghajanyan    schedule 05.04.2011
comment
Спасибо, это лучше иллюстрирует решаемую проблему, чем принятый в настоящее время ответ. - person goertzenator; 02.05.2013
comment
+1: Хороший ответ. Кстати, вместо shared_ptr<S> sp1(new S); может быть предпочтительнее использовать shared_ptr<S> sp1 = make_shared<S>();, см., Например, stackoverflow.com/questions/18301511/ - person Arun; 01.04.2015
comment
Я почти уверен, что последняя строка должна читать shared_ptr<S> sp2 = p->not_dangerous();, потому что здесь ошибка состоит в том, что вы должны создать shared_ptr обычным способом, прежде чем вызывать shared_from_this() в первый раз! Это действительно легко ошибиться! До C ++ 17 было UB вызывать shared_from_this() до того, как будет создан ровно один shared_ptr обычным способом: auto sptr = std::make_shared<S>(); или shared_ptr<S> sptr(new S());. К счастью, начиная с C ++ 17, это вызовет ошибку. - person AnorZaken; 01.09.2016
comment
comment
@AnorZaken Хороший вопрос. Было бы полезно, если бы вы отправили запрос на редактирование, чтобы внести это исправление. Я только что это сделал. Другой полезной вещью было бы, чтобы плакат не выбирал субъективные, контекстно-зависимые имена методов! - person underscore_d; 12.12.2016
comment
Это гениальный ответ, абсолютно гениальный! - person SexyBeast; 19.10.2017
comment
Спасибо, это хорошо объясняет технику, но все еще не совсем понимаю, зачем нам это нужно в реальном сценарии, у вас есть реальный пример? - person baye; 15.05.2021

Вот мое объяснение с точки зрения гаек и болтов (верхний ответ мне не понравился). * Обратите внимание, что это результат исследования источника shared_ptr и enable_shared_from_this, который поставляется с Visual Studio 2012. Возможно, другие компиляторы реализуют enable_shared_from_this по-другому ... *

enable_shared_from_this<T> добавляет частный weak_ptr<T> экземпляр к T, который содержит «один истинный счетчик ссылок» для экземпляра T.

Итак, когда вы впервые создаете shared_ptr<T> на новом T *, этот внутренний weak_ptr T * инициализируется с refcount, равным 1. Новый shared_ptr в основном возвращается к этому weak_ptr.

Затем T может в своих методах вызывать shared_from_this для получения экземпляра shared_ptr<T>, который обращается к тому же внутреннему сохраненному счетчику ссылок. Таким образом, у вас всегда будет одно место, где хранится T* счетчик ссылок, а не несколько shared_ptr экземпляров, которые не знают друг о друге, и каждый думает, что он shared_ptr, который отвечает за счет T ссылок и его удаление, когда их счетчик ссылок достигает нуля.

person mackenir    schedule 01.08.2012
comment
Это правильно, и действительно важной частью является So, when you first create..., потому что это требование (как вы говорите, weak_ptr не инициализируется до тех пор, пока вы не передадите указатель объектов в объект shared_ptr!), И это требование заключается в том, где все может пойти ужасно неправильно, если вы не будете осторожны. Если вы не создадите shared_ptr перед вызовом shared_from_this, вы получите UB - аналогично, если вы создадите более одного shared_ptr, вы также получите UB. Вы должны каким-то образом убедиться, что вы создали shared_ptr ровно один раз. - person AnorZaken; 01.09.2016
comment
Другими словами, вся идея enable_shared_from_this является хрупкой с самого начала, поскольку суть в том, чтобы иметь возможность получить shared_ptr<T> от T*, но на самом деле, когда вы получаете указатель T* t, обычно небезопасно предполагать что-либо о том, что он уже используется или нет, а ошибочное предположение - UB. - person AnorZaken; 01.09.2016
comment
внутренний weak_ptr инициализируется с помощью refcount 1 слабый ptr на T не владеет умным ptr на T. Слабый ptr - это умная ссылка, владеющая достаточным количеством информации, чтобы создать владеющий ptr, который является копией другого владение ptr. Слабый ptr не имеет счетчика ссылок. У него есть доступ к счетчику ссылок, как и у всех реф. - person curiousguy; 29.11.2017

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

Представьте, что класс Client имеет член типа AsynchronousPeriodicTimer:

struct AsynchronousPeriodicTimer
{
    // call this periodically on some thread...
    void SetCallback(std::function<void(void)> callback); 
    void ClearCallback(); // clears the callback
}

struct Client
{
    Client(std::shared_ptr< AsynchronousPeriodicTimer> timer) 
        : _timer(timer)

    {
        _timer->SetCallback(
            [this]
            () 
            {
                assert(this); // what if 'this' is already dead because ~Client() has been called?
                std::cout << ++_counter << '\n';
            }
            );
    }
    ~Client()
    {
        // clearing the callback is not in sync with the timer, and can actually occur while the callback code is running
        _timer->ClearCallback();
    }
    int _counter = 0;
    std::shared_ptr< AsynchronousPeriodicTimer> _timer;
}

int main()
{
    auto timer = std::make_shared<AsynchronousPeriodicTimer>();
    {
        auto client = std::make_shared<Client>(timer);
        // .. some code    
        // client dies here, there is a race between the client callback and the client destructor           
    }
}

Клиентский класс подписывает функцию обратного вызова на периодический таймер. Как только клиентский объект выходит за пределы области видимости, возникает состояние гонки между обратным вызовом и деструктором. Обратный вызов может быть вызван с помощью висячего указателя!

Решение: используйте enable_shared_from_this:

struct Client : std::enable_shared_from_this<Client>
{
Client(std::shared_ptr< AsynchronousPeriodicTimer> timer) 
    : _timer(timer)

    {

    }

    void Init()
    {
        auto captured_self = weak_from_this(); // weak_ptr to avoid cyclic references with shared_ptr

        _timer->SetCallback(
        [captured_self]
        () 
        {
            if (auto self = captured_self.lock())
            {
                // 'this' is guaranteed to be non-nullptr. we managed to promote captured_self to a shared_ptr           
                std::cout << ++self->_counter << '\n';
            }

        }
        );
    }
    ~Client()
    {
        _timer->ClearCallback();
    }
    int _counter = 0;
    std::shared_ptr< AsynchronousPeriodicTimer> _timer;
}

Метод Init отделен от конструктора, поскольку процесс инициализации enable_shared_from_this не завершается до выхода из конструктора. Отсюда дополнительный метод. Как правило, подписываться на асинхронный обратный вызов из конструктора небезопасно, поскольку обратный вызов может обращаться к неинициализированным полям.

person Elad Maimoni    schedule 25.08.2020
comment
В этом конкретном примере я не понимаю, какова добавленная стоимость использования enable_shared_from_this здесь, поскольку Клиент очищает обратный вызов таймера в своем деструкторе? - person Scylardor; 05.09.2020
comment
@Scylardor представьте, что во время выполнения обратного вызова таймера дестуректор вызывается в основном потоке. Обратный вызов может быть уничтожен "this". Фактическая очистка обратного вызова не является ни атомарной, ни синхронизированной с таймером. - person Elad Maimoni; 05.09.2020
comment
О, хорошо, спасибо, что прояснили это. Я забыл о последствиях этого для многопоточности. Теперь это имеет смысл. Отличный пример! - person Scylardor; 08.09.2020
comment
Я бы проголосовал за это как за лучший ответ. Он четко отвечает на вопрос: ПОЧЕМУ enable_shared_from_this полезен? Другие ответы только пытаются объяснить, что делает enable_shared_from_this. - person cyb70289; 17.03.2021
comment
@ cyb70289 обратите внимание, что я только что исправил небольшую ошибку. Рад, что помог. - person Elad Maimoni; 17.03.2021

Обратите внимание, что использование boost :: intrusive_ptr не страдает от этой проблемы. Часто это более удобный способ обойти эту проблему.

person blais    schedule 13.06.2012
comment
Да, но enable_shared_from_this позволяет вам работать с API, который специально принимает shared_ptr<>. На мой взгляд, таким API обычно является Doing It Wrong (так как лучше позволить чему-то более высокому в стеке владеть памятью), но если вы вынуждены работать с таким API, это хороший вариант. - person cdunn2001; 05.06.2013
comment
Лучше оставаться в рамках стандарта, насколько это возможно. - person Sergei; 27.02.2019

Точно так же в C ++ 11 и более поздних версиях: это позволяет включить возможность возвращать this в качестве общего указателя, поскольку this дает вам необработанный указатель.

другими словами, это позволяет вам превратить код, подобный этому

class Node {
public:
    Node* getParent const() {
        if (m_parent) {
            return m_parent;
        } else {
            return this;
        }
    }

private:

    Node * m_parent = nullptr;
};           

в это:

class Node : std::enable_shared_from_this<Node> {
public:
    std::shared_ptr<Node> getParent const() {
        std::shared_ptr<Node> parent = m_parent.lock();
        if (parent) {
            return parent;
        } else {
            return shared_from_this();
        }
    }

private:

    std::weak_ptr<Node> m_parent;
};           
person mchiasson    schedule 15.11.2017
comment
Это будет работать, только если этими объектами всегда управляет shared_ptr. Возможно, вы захотите изменить интерфейс, чтобы убедиться, что это так. - person curiousguy; 29.11.2017
comment
Вы абсолютно правы @curiousguy. Это само собой разумеется. Мне также нравится набирать все мои shared_ptr, чтобы улучшить читаемость при определении моих общедоступных API. Например, вместо std::shared_ptr<Node> getParent const() я обычно выставляю его как NodePtr getParent const(). Если вам абсолютно необходим доступ к внутреннему необработанному указателю (лучший пример: работа с библиотекой C), для этого есть std::shared_ptr<T>::get, о чем я ненавижу упоминать, потому что этот аксессор необработанного указателя использовался слишком много раз по неправильной причине. - person mchiasson; 10.12.2017