std :: shared_ptr в крайнем случае?

60

Я просто смотрел трансляции "Going Native 2012" и заметил обсуждение std::shared_ptr. Я был немного удивлен, услышав несколько отрицательный взгляд Бьярне на std::shared_ptrего комментарий о том, что его следует использовать в качестве «последнего средства», когда время жизни объекта не определено (что, я считаю, по его мнению, должно быть нечастым случаем).

Кто-нибудь захочет объяснить это немного глубже? Как мы можем программировать без std::shared_ptrи до сих пор управлять объектные жизненным раз в безопасном пути?

ronag
источник
8
Не используете указатели? Имея определенного владельца объекта, который управляет жизнью?
Бо Перссон
2
а как насчет данных общего пользования? Трудно не использовать указатели. Также в этом случае std :: shared_pointer сделает грязное «управление временем жизни»
Камил Климек
6
Рассматривали ли вы меньше прислушиваться к представленной рекомендации и больше аргумент за этот совет? Он довольно хорошо объясняет, в какой системе этот совет будет работать.
Никол Болас
@NicolBolas: Я выслушал совет и аргумент, но, очевидно, я не чувствовал, что понял это достаточно хорошо.
Ронаг
В какое время он говорит «последнее средство»? Наблюдая за битом на 36 минуте ( channel9.msdn.com/Events/GoingNative/GoingNative-2012/… ), он говорит, что он настороженно относится к использованию указателей, но в целом он имеет в виду не только shared_ptr и unique_ptr, но даже ' обычный указатель. Он подразумевает, что сами объекты (а не указатели на объекты, выделенные с новым) должны быть предпочтительными. Было ли то, о чем вы думали позже в презентации?
Pharap

Ответы:

56

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

Учитывая это, предпочтительнее использовать объекты с автоматическим сроком хранения и иметь «объекты» подобъектов. В противном случае это unique_ptrможет быть хорошей альтернативой shared_ptr, если не последним средством, каким-то образом попасть в список желательных инструментов.

CB Bailey
источник
5
+1 за то, что вы указали, что проблема не в самой техно (совместное владение), а в трудностях, которые она представляет для нас, простых людей, которые затем должны расшифровать происходящее.
Матье М.
Однако использование такого подхода серьезно ограничит возможность программиста применять шаблоны параллельного программирования к большинству нетривиальных классов ООП (из-за невозможности копирования). Эта проблема поднимается в «Going Native 2013».
Рулонг
48

Мир, в котором живет Бьярне, очень ... академичен, из-за отсутствия лучшего термина. Если ваш код может быть спроектирован и структурирован так, что объекты имеют очень преднамеренные реляционные иерархии, так что отношения владения являются жесткими и несгибаемыми, код перемещается в одном направлении (от высокого уровня к низкому уровню), и объекты общаются только с теми, кто ниже. иерархии, то вы не найдете особой необходимости shared_ptr. Это то, что вы используете в тех редких случаях, когда кто-то должен нарушать правила. Но в противном случае вы можете просто вставить все в vectors или другие структуры данных, которые используют семантику значений, и unique_ptrs для вещей, которые вы должны выделять отдельно.

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

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

Тогда есть weak_ptrs, которые нельзя использовать в отсутствие a shared_ptr. Если ваша система жестко структурирована, вы можете хранить голый указатель на некоторый объект, будучи уверенным в том, что структура приложения гарантирует, что указанный объект переживет вас. Вы можете вызвать функцию, которая возвращает указатель на некоторое внутреннее или внешнее значение (например, найти объект с именем X). В правильно структурированном коде эта функция будет доступна вам только в том случае, если время жизни объекта будет гарантированно превышать ваше; таким образом, хранение этого голого указателя в вашем объекте - это хорошо.

Поскольку такой жесткости не всегда удается достичь в реальных системах, вам нужен какой-то способ разумного обеспечения срока службы. Иногда вам не нужно полное владение; иногда вам просто нужно знать, когда указатель плох или хорош. Вот где weak_ptrприходит. Были случаи, когда я мог использовать unique_ptrили boost::scoped_ptr, но я должен был использовать, shared_ptrпотому что мне специально нужно было дать кому-то «изменчивый» указатель. Указатель, время жизни которого было неопределенным, и они могли запрашивать, когда этот указатель был уничтожен.

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

Может ли это быть сделано с помощью вызова некоторой функции, чтобы получить указатель, а не через weak_ptr? Да, но это может быть легко сломано. Функция, которая возвращает голый указатель, не имеет возможности синтаксически предлагать пользователю не делать что-то вроде сохранения этого указателя в долгосрочной перспективе. Возвращение shared_ptrтакже делает слишком простым его хранение и потенциально продлевает срок службы объекта. Возвращение, weak_ptrоднако, убедительно говорит о том, что хранение полученной shared_ptrвами информации lockявляется ... сомнительной идеей. Это не помешает вам сделать это, но ничто в C ++ не мешает вам взломать код. weak_ptrобеспечивает некоторое минимальное сопротивление от выполнения естественных вещей.

Это не значит, что shared_ptrнельзя злоупотреблять ; это конечно может. Прежде всего unique_ptr, было много случаев, когда я просто использовал a, boost::shared_ptrпотому что мне нужно было передать указатель RAII или поместить его в список. Без ходовой семантики и unique_ptr, boost::shared_ptrбыл единственным реальным решением.

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

Николь Болас
источник
4
+1: посмотрите, например, на boost :: asio. Я думаю, что идея распространяется на многие области, вы можете не знать во время компиляции, какой виджет пользовательского интерфейса или асинхронный вызов является последним, чтобы освободить объект, и с shared_ptr вам не нужно знать. Это очевидно не относится к каждой ситуации, просто еще один (очень полезный) инструмент в наборе инструментов.
Гай Сиртон
3
Немного поздний комментарий; shared_ptrотлично подходит для систем, где c ++ интегрирован с языком сценариев, таким как python. Использование boost::python, подсчет ссылок на стороне c ++ и python сильно взаимодействует; любой объект из c ++ все еще может быть в Python, и он не умрет.
Евдокос
1
Просто для справки, я понимаю, что ни WebKit, ни Chromium не используются shared_ptr. Оба используют свои собственные реализации intrusive_ptr. Я привожу это только потому, что они оба являются реальными примерами больших приложений, написанных на C ++
gman
1
@gman: Я нахожу ваш комментарий очень вводящим в заблуждение, поскольку возражение Страуструпа в shared_ptrравной степени относится к intrusive_ptr: он возражает против всей концепции совместной собственности, а не против какого-либо конкретного написания этой концепции. Таким образом, для целей этого вопроса это два реальных примера больших приложений, которые действительно используют shared_ptr. (Более того, они демонстрируют, что shared_ptrэто полезно, даже если оно не weak_ptr
включено
1
FWIW, чтобы опровергнуть утверждение о том, что Бьярн живет в академическом мире: за всю мою чисто индустриальную карьеру (включая совместное проектирование фондовой биржи G20 и исключительно разработку MOG на 500 тыс. Игроков) я видел только 3 случая, когда нам действительно нужно совместная собственность. Я на 200% с Бьярне здесь.
No-Bugs Hare
38

Я не верю, что когда-либо использовал std::shared_ptr.

Большую часть времени объект ассоциируется с какой-либо коллекцией, к которой он принадлежит на протяжении всего своего жизненного цикла. В этом случае вы можете просто использовать whatever_collection<o_type>или whatever_collection<std::unique_ptr<o_type>>, эта коллекция является членом объекта или автоматической переменной. Конечно, если вам не нужно динамическое количество объектов, вы можете просто использовать автоматический массив фиксированного размера.

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


Никол Болас прокомментировал, что «Если какой-то объект держится за голый указатель, и этот объект умирает ... упс». и «Объекты должны гарантировать, что объект живет через жизнь этого объекта. Только shared_ptrможет сделать это».

Я не покупаю этот аргумент. По крайней мере, это не shared_ptrрешает эту проблему. Что о:

  • Если какая-то хеш-таблица удерживается на объекте, и хеш-код этого объекта изменяется ... упс.
  • Если какая-то функция выполняет итерацию вектора и элемент вставляется в этот вектор ... упс.

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

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

Бен Фойгт
источник
17
@ronag: я подозреваю, что вы начали использовать его там, где необработанный указатель был бы лучше, потому что «необработанные указатели плохие». Но сырые указатели неплохие . Только делать первый, имеющий указатель на объект необработанным указателем, плохо, потому что тогда вам придется вручную управлять памятью, что нетривиально при наличии исключений. Но использование необработанных указателей в качестве дескрипторов или итераторов - это нормально.
Бен Фойгт
4
@BenVoigt: Конечно, проблема с обнаженными указателями заключается в том, что вы не знаете время жизни объектов. Если какой-то объект держится за голый указатель, и этот объект умирает ... упс. Это именно такие вещи shared_ptrи weak_ptrбыли разработаны, чтобы избежать. Бьярне пытается жить в мире, где у всего есть хорошая, явная жизнь, и все построено вокруг этого. И если ты сможешь построить этот мир, прекрасно. Но это не так, как в реальном мире. Объекты должны гарантировать, что объект живет в течение жизни этого объекта. Только shared_ptrмогу сделать это.
Никол Болас
5
@NicolBolas: Это ложная безопасность. Если вызывающая функция не предоставляет обычную гарантию: «Этот объект не будет затронут какой-либо внешней стороной во время вызова функции», тогда обе стороны должны договориться о том, какой тип внешних модификаций разрешен. shared_ptrсмягчает только одну конкретную внешнюю модификацию, и даже не самую распространенную. И это не обязанность объекта гарантировать правильность его времени жизни, если в контракте вызова функции указано иное.
Бен Фойгт
6
@NicolBolas: если функция создает объект и возвращает его по указателю, это должен быть объект unique_ptr, выражающий, что существует только один указатель на объект, и он имеет владельца.
Бен Фойгт
6
@Nicol: если он ищет указатель в некоторой коллекции, он, вероятно, должен использовать любой тип указателя в этой коллекции или необработанный указатель, если коллекция содержит значения. Если он создает объект, и вызывающий объект хочет получить a shared_ptr, он все равно должен вернуть a unique_ptr. Преобразование из unique_ptrв shared_ptrлегко, но обратное логически невозможно.
Бен Фойгт
16

Я предпочитаю думать не в абсолютных терминах (например, «последнее средство»), а относительно проблемной области.

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

На самом деле вы можете:

  1. использовать семантику чистой стоимости . Работает с относительно небольшими объектами, где важны «ценности», а не «идентичности», где можно предположить, что двое, Personимеющие одно и то же, nameявляются одним и тем же человеком (лучше: два представления одного и того же человека ). Срок службы предоставляется машинным стеком, и конец - по существу - не имеет значения для программы (поскольку человек - это его имя , независимо от того, что Personего несет)
  2. использовать объекты , выделенные в стеке , и связанные с ними ссылки или указатели: разрешает полиморфизм и предоставляет время жизни объекта. Нет необходимости в «умных указателях», так как вы гарантируете, что ни один объект не будет «указываться» структурами, которые оставляют в стеке дольше, чем объект, на который они указывают (сначала создайте объект, затем структуры, которые на него ссылаются).
  3. использовать выделенные объекты из управляемой кучи стека : это то, что делают std :: vector и все контейнеры, и std::unique_ptrделает wat (вы можете представить это как вектор с размером 1). Опять же, вы допускаете, что объект начинает существовать (и прекращает свое существование) до (после) структуры данных, к которой он обращается.

Слабость этого метода заключается в том, что типы и количества объектов не могут изменяться во время выполнения вызовов более глубокого уровня стека относительно места их создания. Все эти методы "не справляются" со своей силой во всех ситуациях, когда создание и удаление объекта являются следствием действий пользователя, так что тип времени выполнения объекта неизвестен во время компиляции, и могут быть избыточные структуры, ссылающиеся на объекты пользователь просит удалить из более глубокого вызова функции уровня стека. В этом случае вам необходимо:

  • ввести некоторую дисциплину об управлении объектом и связанными ссылочными структурами или ...
  • перейдите к темной стороне «экранирования времени жизни, основанного на чистом стеке»: объект должен выйти независимо от функций, которые его создали. И должен уйти ... пока они не понадобятся .

C ++ isteslf не имеет никакого собственного механизма для мониторинга этого события ( while(are_they_needed)), поэтому вы должны приблизиться к:

  1. использовать совместное владение : жизнь объектов связана с «счетчиком ссылок»: работает, если «владение» может быть организовано иерархически, происходит сбой, когда могут существовать циклы владения. Это то, что делает std :: shared_ptr. И слабый_птр может быть использован для разрыва цикла. Это работает большую часть времени, но терпит неудачу в большом дизайне, где многие дизайнеры работают в разных командах, и нет четкой причины (что-то вытекает из некоторого требования) о том, кому должно принадлежать то, что (типичный пример - цепочки двойных симпатий: предыдущий из-за того, что следующий ссылается на предыдущий или следующий, владеющий предыдущим и ссылающийся на следующий? В отсутствие требования эти решения эквивалентны, и в большом проекте вы рискуете их смешать
  2. Используйте кучу мусора : вам просто наплевать на жизнь. Вы запускаете сборщик время от времени, и то, что недостижимо, считается "больше не нужно" и ... ну ... хм ... уничтожено? завершена? замороженный ?. Существует множество сборщиков GC, но я никогда не нахожу тот, который действительно знает C ++. У большинства из них свободная память, не заботящаяся об уничтожении объекта.
  3. Используйте сборщик мусора с поддержкой C ++ , с надлежащим интерфейсом стандартных методов. Удачи, чтобы найти это.

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

У сборщика мусора есть стоимость, у shared_ptr меньше, unique_ptr еще меньше, а у управляемых объектов стека очень мало.

Является ли shared_ptr«последним средством»? Нет, это не так: последнее средство - сборщики мусора. shared_ptrна самом деле std::предлагается последнее средство. Но может быть правильным решением, если вы находитесь в ситуации, которую я объяснил.

Эмилио Гаравалья
источник
9

Одна вещь, упомянутая Хербом Саттером в более позднем сеансе, заключается в том, что каждый раз, когда вы копируете a shared_ptr<>, возникает взаимосвязанный шаг / уменьшение, который должен произойти. В многопоточном коде в многоядерной системе синхронизация памяти не является незначительной. Учитывая выбор, лучше использовать либо значение стека, либо a unique_ptr<>и передавать ссылки или необработанные указатели.

Затмение
источник
1
Или передайте shared_ptrссылку lvalue или rvalue ...
ronag
8
Дело в том, что не просто используйте, shared_ptrкак серебряную пулю, которая решит все проблемы с утечкой памяти только потому, что она в стандарте. Это заманчивая ловушка, но все же важно знать о владении ресурсами, и если это владение не является общим, то shared_ptr<>это не лучший вариант.
Затмение
Для меня это наименее важная деталь. Смотрите преждевременную оптимизацию. В большинстве случаев это не должно влиять на решение.
Гай Сиртон
1
@gbjbaanb: да, они находятся на уровне процессора, но в многоядерной системе вы лишаете законной силы кэши и создаете барьеры памяти.
Затмение
4
В игровом проекте, над которым я работал, мы обнаружили, что разница в производительности была очень значительной, и нам понадобилось два различных типа указателей с пересчетом, один из которых был потокобезопасным, а другой - нет.
Kylotan
7

Я не помню, было ли последнее слово «курорт» точным словом, которое он использовал, но я считаю, что реальное значение того, что он сказал, было последним «выбором»: учитывая четкие условия собственности; Unique_ptr, weak_ptr, shared_ptr и даже голые указатели имеют свое место.

Они все согласились с тем, что мы (разработчики, авторы книг и т. Д.) Все в «фазе изучения» C ++ 11, и шаблоны и стили определяются.

В качестве примера, пояснил Херб, нам следует ожидать появления новых выпусков некоторых оригинальных книг по С ++, таких как Effective C ++ (Meyers) и C ++ Coding Standards (Sutter & Alexandrescu), через пару лет, пока опыт отрасли и лучшие практики с C ++ 11 показывает.

Эдди Веласкес
источник
5

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

Еще одна вещь, которую люди забывают (помимо узких мест блокировки / обновления / разблокировки, упомянутых в материале выше), это то, что один shared_ptr не решает проблемы цикла. Вы все еще можете утечь ресурсы с помощью shared_ptr:

Объект A содержит общий указатель на другой объект A. Объект B создает A a1 и A a2 и назначает a1.otherA = a2; и a2.otherA = a1; Теперь, общие указатели объекта B, которые он использовал для создания a1, a2, выходят из области видимости (скажем, в конце функции). Теперь у вас есть утечка - никто больше не ссылается на a1 и a2, но они ссылаются друг на друга, поэтому их количество ссылок всегда равно 1, и вы просочились.

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

Подводя итог: я думаю, что комментарии, на которые ссылается ФП, сводятся к следующему:

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

редактировать: даже если это означает «неизвестно, мне нужно использовать shared_ptr», вы все еще думаете об этом и делаете это намеренно.

скоро
источник
3

Я отвечу из моего опыта с Objective-C, языком, где все объекты подсчитываются и распределяются в куче. Из-за наличия одного способа обработки объектов программисту намного проще. Это позволило определить стандартные правила, которые при соблюдении гарантируют надежность кода и отсутствие утечек памяти. Это также сделало возможным появление умных оптимизаций компилятора, таких как недавний ARC (автоматический подсчет ссылок).

Я хочу сказать, что shared_ptr должен быть вашим первым вариантом, а не последним средством. Используйте подсчет ссылок по умолчанию и другие параметры, только если вы уверены в том, что делаете. Вы будете более продуктивными, а ваш код - более надежным.

Димитрис
источник
1

Я постараюсь ответить на вопрос:

Как мы можем программировать без std :: shared_ptr и при этом безопасно управлять временем жизни объектов?

C ++ имеет большое количество различных способов сделать память, например:

  1. Используйте struct A { MyStruct s1,s2; };вместо shared_ptr в области видимости. Это только для опытных программистов, потому что это требует, чтобы вы понимали, как работают зависимости, и требовало способности контролировать зависимости, достаточные для того, чтобы ограничить их деревом. Порядок классов в заголовочном файле является важным аспектом этого. Кажется, что это использование уже распространено в встроенных типах c ++, но его использование с классами, определенными программистом, кажется менее используемым из-за проблем с зависимостями и порядком классов. У этого решения также есть проблемы с sizeof. Программисты видят в этом проблемы как требование использовать предварительные объявления или ненужные #include, и поэтому многие программисты прибегнут к низкому решению указателей, а затем к shared_ptr.
  2. Используйте MyClass &find_obj(int i);+ clone () вместо shared_ptr<MyClass> create_obj(int i);. Многие программисты хотят создавать фабрики для создания новых объектов. shared_ptr идеально подходит для такого использования. Проблема заключается в том, что оно уже предполагает сложное решение для управления памятью с использованием распределения кучи / свободного хранилища вместо более простого стекового или объектного решения. Хорошая иерархия классов C ++ поддерживает все схемы управления памятью, а не только одну из них. Решение на основе ссылок может работать, если возвращаемый объект хранится внутри содержащего объекта, вместо использования локальной переменной области действия функции. Следует избегать передачи права собственности от фабрики к пользовательскому коду. Копирование объекта после использования find_obj () - это хороший способ справиться с этим - это могут обработать обычные конструкторы копирования и нормальный конструктор (другого класса) с параметром refrerence или clone () для полиморфных объектов.
  3. Использование ссылок вместо указателей или shared_ptrs. Каждый класс C ++ имеет конструкторы, и каждый элемент ссылочных данных должен быть инициализирован. Такое использование позволяет избежать многократного использования указателей и shared_ptrs. Вам просто нужно выбрать, находится ли ваша память внутри объекта или вне его, и выбрать решение struct или эталонное решение на основе решения. Проблемы с этим решением обычно связаны с тем, чтобы избегать параметров конструктора, что является распространенной, но проблемной практикой, и непониманием того, как следует разрабатывать интерфейсы для классов.
ТР1
источник
«Следует избегать передачи права собственности от фабрики к пользовательскому коду». И что происходит, когда это невозможно? "Использование ссылок вместо указателей или shared_ptrs." Нет Указатели могут быть переустановлены. Ссылки не могут. Это накладывает ограничения на время создания того, что хранится в классе. Это не практично для многих вещей. Ваше решение кажется очень жестким и негибким к потребностям более гибкого интерфейса и схемы использования.
Никол Болас
@Nolol Bolas: Как только вы будете следовать приведенным выше правилам, ссылки будут использоваться для зависимостей между объектами, а не для хранения данных, как вы предложили. Зависимости более стабильны, чем данные, поэтому мы никогда не сталкиваемся с проблемой, которую вы рассматривали.
tp1
Вот очень простой пример. У вас есть игровая сущность, которая является объектом. Он должен ссылаться на другой объект, который является целевой сущностью, с которой он должен общаться. Однако цели могут измениться. Цели могут погибнуть в разных точках. И сущность должна быть в состоянии справиться с этими обстоятельствами. Ваш жесткий подход без указателей не может справиться даже с чем-то таким простым, как смена целей, не говоря уже о том, что цель умирает.
Николь Болас
@Nicol Bolas: о, это обрабатывается по-разному; интерфейс класса поддерживает более одной «сущности». Вместо отображения 1: 1 между объектами и объектами вы будете использовать объектный массив. Тогда сущности очень легко умирают, просто удаляя их из массива. Во всей игре имеется только небольшое количество сущностей, и зависимости между массивами меняются не очень часто :)
tp1
2
Нет, unique_ptrлучше всего подходит для заводов. Вы можете превратить unique_ptrв shared_ptr, но логически невозможно пойти в другом направлении.
Бен Фойгт