shared_ptr магия :)

89

Мы с мистером Лидстремом поссорились :)

Г-н Лидстрём утверждает, что конструкция shared_ptr<Base> p(new Derived);не требует наличия виртуального деструктора в Base:

Армен Цирунян : «Правда? Будет ли shared_ptr правильно очищаться? Не могли бы вы в этом случае продемонстрировать, как этот эффект может быть реализован?»

Даниэль Лидстрём : « shared_ptr использует свой собственный деструктор для удаления экземпляра Concrete. В сообществе C ++ он известен как RAII. Я советую вам узнать все, что можно о RAII. Это значительно упростит программирование на C ++ при использовании RAII во всех ситуациях ».

Армен Цирунян : «Я знаю о RAII, и я также знаю, что в конечном итоге деструктор shared_ptr может удалить сохраненный px, когда pn достигнет 0. Но если у px есть указатель статического типа и указатель Baseдинамического типа на Derived, то, если Baseнет виртуального деструктора, это приведет к неопределенному поведению. Исправьте меня, если я ошибаюсь. "

Даниэль Лидстрём : « shared_ptr знает, что статический тип - это Concrete. Он знает это, так как я передал его в его конструкторе! Это немного похоже на магию, но я могу вас заверить, что это сделано по дизайну и очень красиво».

Итак, судите нас. Как возможно (если это возможно) реализовать shared_ptr, не требуя, чтобы полиморфные классы имели виртуальный деструктор? заранее спасибо

Армен Цирунян
источник
3
Вы могли быть связаны с исходной цепочкой .
Дарин Димитров
8
Еще одна интересная вещь: shared_ptr<void> p(new Derived)он также уничтожит Derivedобъект своим деструктором, независимо от того, есть он virtualили нет.
далле
7
Отличный способ задать вопрос :)
rubenvb
5
Несмотря на то, что shared_ptr позволяет это, очень плохая идея создавать класс в качестве основы без виртуального dtor. Комментарии Дэниела о RAII вводят в заблуждение - они не имеют к этому никакого отношения, - но процитированный разговор звучит как простое недопонимание (и неверное предположение о том, как работает shared_ptr).
6
Не RAII, а скорее стирает тип деструктора. Вы должны быть осторожны, потому что shared_ptr<T>( (T*)new U() )where struct U:Tне будет делать то, что нужно (и это можно легко сделать косвенно, например, функция, которая принимает T*и передает a U*)
Якк - Адам Неврамонт

Ответы:

74

Да, таким образом можно реализовать shared_ptr. Boost делает, и стандарт C ++ 11 также требует такого поведения. В качестве дополнительной гибкости shared_ptr управляет не только счетчиком ссылок. Так называемый удалитель обычно помещается в тот же блок памяти, который также содержит счетчики ссылок. Но самое интересное в том, что тип этого средства удаления не является частью типа shared_ptr. Это называется «стиранием типа» и в основном представляет собой тот же метод, который используется для реализации «полиморфных функций» boost :: function или std :: function для сокрытия фактического типа функтора. Чтобы ваш пример заработал, нам понадобится шаблонный конструктор:

template<class T>
class shared_ptr
{
public:
   ...
   template<class Y>
   explicit shared_ptr(Y* p);
   ...
};

Итак, если вы используете это со своими классами Base и Derived ...

class Base {};
class Derived : public Base {};

int main() {
   shared_ptr<Base> sp (new Derived);
}

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

В стандарте C ++ 11 об этом конструкторе (20.7.2.2.1) говорится следующее:

Требует: p должно быть конвертируемым в T*. Yдолжен быть законченным типом. Выражение delete pдолжно быть правильно сформировано, иметь четко определенное поведение и не должно вызывать исключений.

Эффекты: создает shared_ptrобъект, которому принадлежит указатель p.

А для деструктора (20.7.2.2.2):

Эффекты: Если *thisэто пустая или акция собственности с другим shared_ptrэкземпляром ( use_count() > 1), нет никаких побочных эффектов. В противном случае, если *thisвладелец объекта pи Deleter d, d(p)называется. В противном случае, if *thisвладеет указателем pи delete pвызывается.

(выделение жирным шрифтом принадлежит мне).

Sellibitze
источник
the upcoming standard also requires this behaviour: (а) Какой стандарт и (б) вы можете дать ссылку (на стандарт)?
kevinarpe
Я просто хочу добавить комментарий к ответу @sellibitze, так как у меня недостаточно очков add a comment. ИМО, больше Boost does thisчем the Standard requires. Я не думаю, что Стандарт требует этого из того, что я понимаю. Говоря о примере @sellibitze «s shared_ptr<Base> sp (new Derived);, требует от constructorпросто попросить delete Derivedбыть четко определены и хорошо сформированы. Для спецификации destructorсуществует также p, но я не думаю, что он относится к pв спецификации constructor.
Lujun Weng
28

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

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

void DeleteDerived(Derived* d) { delete d; } // EDIT: no conversion needed.

shared_ptr<Base> p(new Derived, DeleteDerived);

p вызовет DeleteDerived для уничтожения указанного объекта. Реализация делает это автоматически.

Яков Галка
источник
4
+1 за замечание о неполных типах, очень удобно при использовании shared_ptrв качестве атрибута.
Matthieu M.
16

Просто,

shared_ptr использует специальную функцию удаления, которая создается конструктором, который всегда использует деструктор данного объекта, а не деструктор Base, это небольшая работа с метапрограммированием шаблона, но оно работает.

Что-то такое

template<typename SomeType>
shared_ptr(SomeType *p)
{
   this->destroyer = destroyer_function<SomeType>(p);
   ...
}
Артём
источник
1
хм ... интересно, я начинаю в это верить :)
Армен Цирунян
1
@Armen Tsirunyan Вы должны были заглянуть в описание дизайна shared_ptr, прежде чем начинать обсуждение. Этот «захват удалителя» - одна из важнейших особенностей shared_ptr ...
Пол Михалик
6
@ paul_71: Я с тобой согласен. С другой стороны, я считаю, что это обсуждение было полезно не только для меня, но и для других людей, которые не знали этого факта о shared_ptr. Так что я думаю, что это не было большим грехом начать эту
ветку
3
@Armen Конечно, нет. Скорее, вы хорошо поработали, указав на эту действительно очень важную особенность shared_ptr <T>, за которой часто наблюдают даже опытные разработчики C ++.
Пол Михалик