Почему std :: enable_shared_from_this допускает несколько экземпляров std :: shared_ptr? - PullRequest
2 голосов
/ 05 июня 2019

Есть несколько вопросов, которые касаются поведения std::enable_shared_from_this, но я не думаю, что это дубликат.

Классы, которые наследуются от std::enable_shared_from_this, содержат std::weak_ptr член. Когда приложение создает std::shared_ptr, указывающий на подкласс std::enable_shared_from_this, конструктор std::shared_ptr проверяет std::weak_ptr и, если он не инициализирован, инициализирует его и использует блок управления std::weak_ptr для std::shared_ptr , Однако, если std::weak_ptr уже инициализирован, конструктор просто создает новый std::shared_ptr с новым блоком управления. Это приводит к сбою приложения, когда счетчик ссылок одного из двух экземпляров std::shared_ptr обнуляется и удаляет базовый объект.

struct C : std::enable_shared_from_this<C> {};

C *p = new C();
std::shared_ptr<C> p1(p);

// Okay, p1 and p2 both have ref count = 2
std::shared_ptr<C> p2 = p->shared_from_this();

// Bad: p3 has ref count 1, and C will be deleted twice
std::shared_ptr<C> p3(p);

Мой вопрос: почему библиотека ведет себя так? Если конструктор std::shared_ptr знает, что объект является подклассом std::enable_shared_from_this и пытается проверить поле std::weak_ptr, почему он не всегда использует один и тот же блок управления для нового std::shared_ptr, что позволяет избежать потенциального сбоя?

И в этом отношении, почему метод shared_from_this терпит неудачу, когда элемент std::weak_ptr не инициализируется, вместо того, чтобы просто инициализировать его и вернуть std::shared_ptr?

Кажется странным, что библиотека работает так, как она работает, так как она терпит неудачу в ситуациях, когда она может легко преуспеть. Мне интересно, были ли конструктивные соображения / ограничения, которые я не понимаю.

Я использую Clang 8.0.0 в режиме C ++ 17.

Ответы [ 4 ]

5 голосов
/ 05 июня 2019

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

С вашей точки зрения это выглядит логичным. Давайте на минутку предположим, что C является частью библиотеки, которую вы поддерживаете, а использование C является частью пользователя вашей библиотеки.

struct C : std::enable_shared_from_this<C> {};

C *p = new C();
std::shared_ptr<C> p1(p);
std::shared_ptr<C> p3(p); // Valid given your assumption

Теперь вы нашли способ больше не нуждаться в enable_shared_from_this, и в следующей версии вашей библиотеки это будет обновлено до:

struct C {};

C *p = new C();
std::shared_ptr<C> p1(p);
std::shared_ptr<C> p3(p); // Now a bug

Внезапно совершенно корректный код становится недействительным без каких-либо ошибок / предупреждений компилятора из-за обновления вашей библиотеки. По возможности это должно быть предотвращено.

В то же время это вызовет много путаницы. Причина, в зависимости от реализации класса, который вы помещаете в shared_ptr, это либо определенное, либо неопределенное поведение. Менее запутанно делать его неопределенным каждый раз.

enable_shared_from_this - это стандартный обходной путь для получения shared_ptr, если у вас нет shared_ptr. Классический пример:

 struct C : std::enable_shared_from_this<C>
 {
     auto func()
     {
         return std::thread{[c = this->shared_from_this()]{ /*Do something*/ }};
     }

     NonCopyable nc;
 };

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

0 голосов
/ 09 июня 2019

Создание вторичного интеллектуального указателя, который либо фактически не принадлежит (он ничего не делает, когда последняя копия сбрасывается / уничтожается), либо переносит копию исходного интеллектуального указателя в блоке управления (в объекте удаления) так, чтобы при счетчик вторичных ссылок становится равным нулю, первичный счетчик ссылок уменьшается, это очень редкое явление и, вероятно, сбивает с толку большинство программистов, но по своей сути это не является незаконным. (И я думаю, что в особых случаях можно привести веские аргументы в пользу этого паттерна.)

С другой стороны, существование shared_from_this настоятельно предполагает, что есть только один владелец shared_ptr, и поэтому, вероятно, следует избегать shared_from_this, когда ожидается несколько наборов std::shared_ptr. Явное управление самоссылочными не владеющими указателями является более безопасным, поскольку оно делает такие проблемы очевидными в пользовательском коде, в отличие от неявного поведения std::enable_shared_from_this.

0 голосов
/ 07 июня 2019

Это не ответ на вопрос, а скорее ответ пользователю jvapen , основанный на его ответе на этот вопрос.

Вы сказали это в своем ответе:

struct C {};

C *p = new C();
std::shared_ptr<C> p1(p);
std::shared_ptr<C> p3(p); // Now a bug

Чего я здесь не вижу, так это того, как строка 5 std::shared_ptr<C> p3(p); теперь является ошибкой.Согласно cppreference: shared_ptr они специально утверждают это:

std::shared_ptr - это интеллектуальный указатель, который сохраняет общее владение объектом через указатель.Несколько shared_ptr объектов могут иметь один и тот же объект.

0 голосов
/ 05 июня 2019

Создание двух shared_ptr s для одного и того же указателя является неопределенным поведением и не имеет ничего общего с std::enable_shared_from_this. Ваш код должен быть:

struct C : std::enable_shared_from_this<C> {};

C *p = new C();
std::shared_ptr<C> p1(p);

std::shared_ptr<C> p2 = p->shared_from_this();

std::shared_ptr<C> p3(p1);
...