В чем полезность `enable_shared_from_this`?


350

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

Ответы:


363

Это позволяет вам получить действительный 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. Вы также можете получить его оттуда, а также от повышения.


203
+1. Ключевым моментом является то, что «очевидная» техника простого возврата shared_ptr <Y> (this) не работает, потому что это приводит к созданию нескольких отдельных объектов shared_ptr с отдельным количеством ссылок. По этой причине вы никогда не должны создавать более одного shared_ptr из одного и того же необработанного указателя .
j_random_hacker

3
Следует отметить , что в C ++ 11 и более поздних версий , это вполне допустимо использовать std::shared_ptrконструктор на сырой указатель , если он наследует от std::enable_shared_from_this. Я не знаю, была ли обновлена ​​семантика Boost для поддержки этого.
Матфея

6
@ MatthewHolder У вас есть цитата для этого? На cppreference.com я читаю «Создание std::shared_ptrобъекта для объекта, который уже управляется другим std::shared_ptr, не будет обращаться к внутренне хранимой слабой ссылке и, следовательно, приведет к неопределенному поведению». ( en.cppreference.com/w/cpp/memory/enable_shared_from_this )
Турбьёрн Линдейер,

5
Почему ты не можешь просто сделать shared_ptr<Y> q = p?
Дэн М.

2
@ ThorbjørnLindeijer, вы правы, это C ++ 17 и позже. Некоторые реализации следовали семантике C ++ 16 до ее выпуска. Должна быть правильная обработка для C ++ 11 - C ++ 14 std::make_shared<T>.
Мэтью

199

из статьи доктора Доббса о слабых указателях, я думаю, этот пример легче понять (источник: 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_ptобъект r 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
}

16
Спасибо, это иллюстрирует решаемую проблему лучше, чем принятый в настоящее время ответ.
goertzenator

2
+1: хороший ответ. Кроме того, вместо того, shared_ptr<S> sp1(new S);чтобы использовать его shared_ptr<S> sp1 = make_shared<S>();, см., Например, stackoverflow.com/questions/18301511/…
Арун

4
Я почти уверен, что последняя строка должна читаться, 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 и далее, будет выбрасывать.
AnorZaken


2
@AnorZaken Хороший вопрос. Было бы полезно, если бы вы отправили запрос на редактирование, чтобы исправить это. Я только что сделал это. Другой полезной вещью было бы для автора не выбирать субъективные, контекстно-зависимые имена методов!
underscore_d

30

Вот мое объяснение с точки зрения гайки и болты (верхний ответ не «щелкнул» со мной). * Обратите внимание, что это результат изучения источника для 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 *, внутренний слабый_птр этого T * инициализируется с повторным счетом 1. Новое в shared_ptrосновном возвращается к этому weak_ptr.

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


1
Это правильно, и действительно важная часть заключается в So, when you first create...том, что это требование (как вы говорите, weak_ptr не инициализируется до тех пор, пока вы не передадите указатель объектов в ctor shared_ptr!), И это требование - то, где все может пойти не так, если вы не осторожно Если вы не создали shared_ptr перед вызовом, shared_from_thisвы получите UB - аналогично, если вы создадите более одного shared_ptr, вы получите и UB. Вы должны как-то убедиться, что вы создали shared_ptr ровно один раз.
AnorZaken

2
Другими словами, сама идея enable_shared_from_thisхрупка с самого начала, так как смысл в том, чтобы иметь возможность получить a shared_ptr<T>от a T*, но на самом деле, когда вы получаете указатель, T* tобычно небезопасно предполагать, что что-то о нем уже передано или нет, и сделать неправильное предположение UB.
AnorZaken

« Внутренний слабый_птр инициализируется с повторным счетом 1 ». Слабый ptr для T не владеет интеллектуальным ptr для T. Слабый ptr - это владелец smart ref для достаточного количества информации, чтобы сделать собственный ptr, который является «копией» другого владельца ptr. Слабый ptr не имеет счетчика ссылок. Он имеет доступ к счету ссылок, как и все владельцы ссылок.
любопытный парень

3

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


Да, но enable_shared_from_thisпозволяет работать с API, который специально принимает shared_ptr<>. На мой взгляд, такой API, как правило, делает неправильно, так как лучше, чтобы в стеке было что-то более высокое, но если вы вынуждены работать с таким API, это хороший вариант.
cdunn2001

2
Лучше оставаться в рамках стандарта как можно больше.
Сергей

3

Это точно так же в 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;
};           

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

1
Вы абсолютно правы @curiousguy. Это само собой разумеется. Мне также нравится определять все мои shared_ptr для улучшения читаемости при определении моих общедоступных API. Например, вместо того std::shared_ptr<Node> getParent const(), чтобы я обычно выставлял это как NodePtr getParent const()вместо. Если вам абсолютно необходим доступ к внутреннему необработанному указателю (лучший пример: работа с библиотекой C), то есть std::shared_ptr<T>::getто, о чем я не хочу упоминать, потому что я использовал этот обработчик необработанных указателей слишком много раз по неправильной причине.
mchiasson

-3

Другой способ - добавить weak_ptr<Y> m_stubучастника в class Y. Затем написать:

shared_ptr<Y> Y::f()
{
    return m_stub.lock();
}

Полезно, когда вы не можете изменить класс, из которого вы производите (например, расширение библиотеки других людей). Не забудьте инициализировать элемент, например, с помощью m_stub = shared_ptr<Y>(this), он действителен даже во время конструктора.

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

Редактировать: как правильно указал пользователь nobar, код уничтожит объект Y, когда присваивание будет завершено, а временные переменные уничтожены. Поэтому мой ответ неверен.


4
Если ваше намерение заключается в том, чтобы создать объект, shared_ptr<>который не удаляет его pointee, это излишне. Вы можете просто сказать, return shared_ptr<Y>(this, no_op_deleter);где no_op_deleterунарный функциональный объект берет Y*и ничего не делает.
Джон Цвинк

2
Вряд ли это рабочее решение. m_stub = shared_ptr<Y>(this)построит и немедленно уничтожит временный shared_ptr из этого. Когда это утверждение закончится, thisбудут удалены и все последующие ссылки будут болтаться.
Нобар

2
Автор признает, что этот ответ неправильный, поэтому он может просто удалить его. Но он последний раз заходил через 4,5 года, так что вряд ли это удастся - может ли кто-то с более высокими полномочиями удалить эту красную сельдь?
Том Гудфеллоу

если вы посмотрите на реализацию enable_shared_from_this, она сохраняет weak_ptrсебя (заполняется ctor), возвращаемую как shared_ptrпри вызове shared_from_this. Другими словами, вы дублируете то, что enable_shared_from_thisуже обеспечивает.
mchiasson
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.