Ответ 1
Здесь обсуждается ряд вопросов, поэтому я переписал свой пост, чтобы лучше конденсировать эту информацию.
Автоматическое обнаружение цикла
Ваша идея состоит в том, чтобы иметь circle_ptr
умный указатель (я знаю, что вы хотите добавить его в shared_ptr
, но проще сказать о новом типе, чтобы сравнить эти два). Идея состоит в том, что если тип, который интеллектуальный указатель связан с каким-то cycle_detector_mixin
, активирует автоматическое обнаружение цикла.
Этот mixin также требует, чтобы тип реализовал интерфейс. Он должен предоставить возможность перечислять все экземпляры circle_ptr
, непосредственно принадлежащие этому экземпляру. И он должен предоставить средства для аннулирования одного из них.
Я утверждаю, что это очень непрактичное решение этой проблемы. Он чрезмерно хрупкий и требует огромного количества ручной работы от пользователя. И поэтому он не подходит для включения в стандартную библиотеку. И вот некоторые причины.
Детерминизм и стоимость
"Это приведет к детерминированному уничтожению циклических объектов
shared_ptr
". Обнаружение цикла происходит только тогда, когда счетчик ссылокshared_ptr
падает до нуля, поэтому программист контролирует, когда это происходит. Поэтому он не будет недетерминированным. Его поведение было бы предсказуемым - оно уничтожило бы все известные в настоящее время циклы из этого указателя. Это сродни тому, как деструкторshared_ptr
разрушает базовый объект, когда его отсчет отсчета падает до нуля, несмотря на возможность этого вызвать "недетерминированный" каскад дальнейших разрушений.
Это правда, но не полезно.
Существует существенная разница между детерминизмом регулярного разрушения и детерминизма того, что вы предлагаете. А именно: shared_ptr
дешево.
shared_ptr
деструктор выполняет атомный декремент, за которым следует условный тест, чтобы узнать, уменьшилось ли значение до нуля. Если это так, вызывается деструктор и освобождается память. Что это.
То, что вы предлагаете, делает это более сложным. В худшем случае каждый раз, когда a circle_ptr
уничтожается, код должен пройти через структуры данных, чтобы определить, существует ли цикл. Большую часть времени циклов не будет. Но он все равно должен искать их, просто чтобы убедиться. И он должен делать это каждый раз, когда вы уничтожаете circle_ptr
.
Python et. и др. обойти эту проблему, потому что они встроены в язык. Они могут видеть все, что происходит. И поэтому они могут обнаруживать, когда указатель назначается во время выполнения этих назначений. Таким образом, такие системы постоянно выполняют небольшие объемы работы для создания циклических цепей. Как только ссылка исчезнет, она может смотреть на свои структуры данных и предпринимать действия, если это создает циклическую цепочку.
Но то, что вы предлагаете, - это функция библиотеки, а не функция языка. И типы библиотек не могут этого сделать. Вернее, они могут, но только с помощью.
Помните: экземпляр circle_ptr
не может знать подобъект, членом которого он является. Он не может автоматически преобразовать указатель на себя в указатель на его собственный класс. И без этой возможности он не может обновлять структуры данных в cycle_detector_mixin
, который владеет им, если он переназначен.
Теперь это можно сделать вручную, но только с помощью его собственного экземпляра. Это означает, что circle_ptr
потребуется набор конструкторов, которым задан указатель на свой собственный экземпляр, который происходит от cycle_detector_mixin
. И тогда его operator=
сможет сообщить своему владельцу, что он обновлен. Очевидно, что назначение копирования/перемещения не копирует/перемещает указатель экземпляра экземпляра.
Конечно, для этого требуется, чтобы экземпляр-владелец дал указатель на себя для каждого созданного circle_ptr
. В каждой конструкторской функции, создающей экземпляры circle_ptr
. Внутри себя и любых классов, которыми он владеет, а также не управляется cycle_detection_mixin
. Безошибочно. Это создает некоторую хрупкость в системе; ручное усилие должно быть затрачено для каждого экземпляра circle_ptr
, принадлежащего типу.
Это также требует, чтобы circle_ptr
содержал 3 типа указателя: указатель на объект, который вы получаете от operator*
, указатель на фактическое управляемое хранилище и указатель на этого владельца экземпляра. Причина, по которой экземпляр должен содержать указатель на его владельца, заключается в том, что это данные экземпляра, а не информация, связанная с самим блоком. Это экземпляр circle_ptr
, который должен быть способен сообщить его владельцу, когда он восстанавливается, поэтому экземпляру нужны эти данные.
И это должны быть статические накладные расходы. Вы не можете знать, когда экземпляр circle_ptr
находится внутри другого типа, а когда нет. Таким образом, каждый circle_ptr
, даже те, которые не используют функции обнаружения цикла, должны нести эту стоимость с тремя указателями.
Таким образом, это не только требует большой степени хрупкости, но и дорогостоящего, раздувающего размер шрифта на 50%. Замена shared_ptr
на этот тип (или более на то, добавление shared_ptr
с помощью этой функции) просто неприемлемо.
В плюсе вам больше не нужны пользователи, которые получают от cycle_detector_mixin
, чтобы реализовать способ получения списка экземпляров circle_ptr
. Вместо этого у вас есть регистр класса с экземплярами circle_ptr
. Это позволяет экземплярам circle_ptr
, которые могут быть циклическими, чтобы напрямую разговаривать с их владельцем cycle_detector_mixin
.
Так что-то.
Инкапсуляция и инварианты
Необходимость указывать классу, чтобы аннулировать один из его объектов circle_ptr
, в корне меняет способ взаимодействия класса с любым из его членов circle_ptr
.
Инвариант - это некоторое состояние, которое предполагает кусок кода, истинно, потому что для него должно быть логически невозможно, чтобы оно было ложным. Если вы проверите, что переменная const int
равнa > 0, вы установили инвариант для более позднего кода, чтобы это значение было положительным.
Инкапсуляция существует, чтобы вы могли создавать инварианты внутри класса. Только конструкторы этого не могут сделать, потому что внешний код может изменять любые значения, хранящиеся в классе. Инкапсуляция позволяет предотвратить внесение изменений в внешний код. И поэтому вы можете развить инварианты для различных данных, хранящихся в классе.
Это инкапсуляция.
При a shared_ptr
можно построить инвариант вокруг существования такого указателя. Вы можете создать свой класс, чтобы указатель никогда не был нулевым. И поэтому никто не должен проверять, что он является нулевым.
Это не тот случай с circle_ptr
. Если вы реализуете cycle_detector_mixin
, то ваш код должен иметь возможность обрабатывать случай, когда любой из этих экземпляров circle_ptr
становится нулевым. Поэтому ваш деструктор не может предположить, что они действительны, а также не может любой код, который вызовет ваш деструктор, сделать это предположение.
Поэтому ваш класс не может установить инвариант с объектом, на который указывает circle_ptr
. По крайней мере, если это часть cycle_detector_mixin
с соответствующей регистрацией и еще что-то.
Вы можете утверждать, что ваш дизайн технически не разрушает инкапсуляцию, поскольку экземпляры circle_ptr
все еще могут быть закрытыми. Но класс охотно отказывается от инкапсуляции в систему обнаружения циклов. И поэтому класс уже не может обеспечить определенные виды инвариантов.
Это звучит как нарушение инкапсуляции для меня.
Безопасность резьбы
Чтобы получить доступ к weak_ptr
, пользователь должен lock
его. Это возвращает a shared_ptr
, который гарантирует, что объект останется в живых (если он еще был). Блокировка - это атомная операция, точно так же, как эталонное приращение/декрементирование. Таким образом, это все поточно-безопасное.
circle_ptr
может быть не очень надежным потоком. Возможно, что circle_ptr
станет недействительным из другого потока, если другой поток выпустил последнюю некруглую ссылку на него.
Я не совсем уверен в этом. Возможно, такие обстоятельства появляются только в том случае, если у вас уже есть гонка данных об уничтожении объекта или используются ссылки, не связанные с владельцем. Но я не уверен, что ваш дизайн может быть потокобезопасным.
Факторы вирульности
Эта идея невероятно вирусная. Каждый другой тип, где могут возникать циклические ссылки, должен реализовывать этот интерфейс. Это не то, что вы можете наложить на один тип. Чтобы получить преимущества, каждый тип, который может участвовать в циклической ссылке, должен использовать его. Постоянно и правильно.
Если вы попытаетесь сделать circle_ptr
требуемым, чтобы объект, которым он управляет, реализует cycle_detector_mixin
, то вы не можете использовать такой указатель с любым другим типом. Это не было бы заменой (или увеличением) shared_ptr
. Таким образом, компилятор не может помочь обнаружить случайное злоупотребление.
Конечно, есть случайные злоупотребления make_shared_from_this
, которые не могут быть обнаружены компиляторами. Однако это не вирусная конструкция. Поэтому это проблема только для тех, кому нужна эта функция. Напротив, единственный способ получить выгоду от cycle_detector_mixin
- использовать его как можно более полно.
Не менее важно, потому что эта идея настолько вирусна, вы будете ее использовать много. И поэтому вы гораздо чаще сталкиваетесь с проблемой множественного наследования, чем пользователи make_shared_from_this
. И это не второстепенная проблема. Тем более что cycle_detector_mixin
скорее всего будет использовать static_cast
для доступа к производному классу, поэтому вы не сможете использовать виртуальное наследование.
Суммирование
Итак, вот что вы должны сделать, обязательно, чтобы обнаружить циклы, ни один из которых не проверяет компилятор:
-
Каждый класс, участвующий в цикле, должен быть получен из
cycle_detector_mixin
. -
Каждый раз, когда класс
cycle_detector_mixin
-derived строит экземплярcircle_ptr
внутри себя (прямо или косвенно, но не внутри класса, который сам происходит изcycle_detector_mixin
), передайте указатель на себяcycle_ptr
. -
Не предполагайте, что субобъект
cycle_ptr
класса действителен. Возможно, даже в той степени, в которой он становится недействительным в функции-члене благодаря проблемам с потоками.
И вот издержки:
-
Структуры данных, определяющие цикличность в
cycle_detector_mixin
. -
Каждый
cycle_ptr
должен быть на 50% больше, даже те, которые не используются для обнаружения цикла.
Заблуждения о собственности
В конечном счете, я думаю, что вся эта идея сводится к ошибочному представлению о том, что действительно означает shared_ptr
.
"Детектор циклов не нужен, потому что циклы не так часто, и их можно легко избежать, используя
std::weak_ptr
". Циклы фактически легко возникают во многих простых структурах данных - например, дерево, в котором дети имеют обратный указатель на родителя, или двусвязный список. В некоторых случаях циклы между гетерогенными объектами в сложных системах формируются лишь изредка с определенными образцами данных и их трудно предсказать и избежать. В некоторых случаях далеко не очевидно, какой указатель заменить на слабый вариант.
Это очень распространенный аргумент для GC общего назначения. Проблема с этим аргументом заключается в том, что он обычно делает предположение о том, что использование интеллектуальных указателей просто недопустимо.
Использовать shared_ptr
означает что-то. Если класс хранит shared_ptr
, который представляет, что класс имеет право собственности на этот объект.
Итак, объясните это: почему node в связанном списке должен владеть как следующим, так и предыдущим узлами? Почему дочерний элемент node в дереве должен владеть родительским node? О, они должны иметь возможность ссылаться на другие узлы. Но им не нужно контролировать срок их жизни.
Например, я бы использовал дерево node как массив unique_ptr
для своих детей с одним указателем на родителя. Обычный указатель, а не умный указатель. В конце концов, если дерево построено правильно, родитель будет иметь своих детей. Поэтому, если существует дочерний элемент node, он должен иметь родительский node; ребенок не может существовать без наличия действительного родителя.
В двойном связанном списке у меня может быть указатель слева unique_ptr
, при этом правильный указатель будет правильным. Или наоборот; один путь не лучше, чем другой.
Ваш менталитет, кажется, состоит в том, что мы всегда должны использовать shared_ptr
для вещей, и просто дайте автоматической системе понять, как бороться с проблемами. Будь то круговые ссылки или что-то еще, просто дайте системе понять это.
Это не то, для чего shared_ptr
. Цель умных указателей заключается не в том, что вы больше не думаете о собственности; это то, что вы можете прямо выражать отношения собственности в коде.
В целом
Как можно улучшить использование weak_ptr
для прерывания циклов? Вместо того, чтобы распознавать, когда могут произойти циклы и делать дополнительную работу, вы теперь выполняете кучу дополнительной работы повсюду. Работа, которая чрезвычайно кратковременна; если вы сделаете это неправильно, вам не лучше, чем если бы вы пропустили место, где вы должны были использовать weak_ptr
. Только это хуже, потому что вы, вероятно, думаете, что ваш код в безопасности.
Иллюзия безопасности хуже, чем никакой безопасности. По крайней мере, последнее делает вас осторожным.
Не могли бы вы реализовать что-то вроде этого? Возможно. Является ли он подходящим типом для стандартной библиотеки? Нет. Это слишком хрупкое. Вы должны внедрять его правильно, во все времена, во всех отношениях, везде, где могут появляться циклы... или вы ничего не получаете.
Авторитетные ссылки
Не может быть никаких авторитетных ссылок на то, что было никогда не предлагалось, не предлагалось или даже не предполагалось для стандартизации. Boost не имеет такого типа, и такие конструкции никогда не рассматривалисьдля boost::shared_ptr
. Даже самая первая смарт-бумага указателя (PDF) никогда не рассматривала возможность. Тема расширения shared_ptr
, чтобы автоматически обрабатывать циклы через какое-то ручное усилие, никогда не обсуждалась даже на стандартных форумах предложений, где далеко Были обсуждены идеи глупого.
Ближайшая к ссылке, которую я могу предоставить, это этот документ с 1994 года о смарт-указателе с подсчетом ссылок. В этой статье в основном говорится о том, чтобы сделать эквивалент shared_ptr
и weak_ptr
части языка (это было в первые дни, они даже не думали, что можно написать shared_ptr
, который позволил бы лить shared_ptr<T>
до shared_ptr<U>
, когда U
является базой T
). Но даже в этом, в частности, говорится, что циклы не будут собираться. Он не тратит много времени на то, почему нет, но он это утверждает:
Однако циклы собранных объектов с очисткой функции проблематичны. Если A и B достижимы из друг друга, то уничтожение одного из них сначала нарушит заказная гарантия, оставив свисающий указатель. Если коллекционер прерывает цикл произвольно, программисты не имеют реальной гарантии порядка и тонкой, зависящей от времени могут возникнуть ошибки. На сегодняшний день никто не разработал безопасный, общее решение этой проблемы [Hayes 92].
Это, по сути, проблема инкапсуляции/инварианта, на которую я указал: создание элемента указателя типа invalid нарушает инвариант.
Таким образом, мало кто даже рассмотрел возможность, и те немногие, кто быстро отбросил ее, были непрактичными. Если вы действительно верите, что они ошибаются, единственный лучший способ доказать это - реализовать его самостоятельно. Затем предложите его для стандартизации.