Язык системного программирования Rust использует парадигму владения, чтобы обеспечить во время компиляции нулевую стоимость времени выполнения, когда ресурс должен быть освобожден (см. «Руст Книга о собственности»).
В C ++ мы обычно используем умные указатели для достижения той же цели — скрыть сложность управления распределением ресурсов. Хотя есть несколько отличий:
Мой вопрос: как мы можем эмулировать парадигму владения в C ++ в следующих ограничениях:
Редактировать:
Учитывая комментарии, мы можем сделать вывод:
Можете свернуть свои собственные, создавая типы оберток вокруг shared_ptr и weak_ptr:
Вы не можете сделать это с проверками во время компиляции вообще. В системе типов C ++ отсутствует какой-либо способ рассуждать о том, когда объект выходит из области видимости, перемещается или уничтожается, а тем более превращает это в ограничение типа.
Что вы могли бы сделать, это иметь вариант unique_ptr
это сохраняет счетчик количества «заимствований», активных во время выполнения. Вместо get()
возвращая необработанный указатель, он возвращает умный указатель, который увеличивает этот счетчик при построении и уменьшает его при уничтожении. Если unique_ptr
уничтожается, пока счетчик не равен нулю, по крайней мере, вы знаете, что кто-то где-то сделал что-то не так.
Однако это не надежное решение. Независимо от того, как сильно вы пытаетесь предотвратить это, всегда найдутся способы получить необработанный указатель на базовый объект, а затем игра закончится, поскольку этот необработанный указатель может легко пережить умный указатель и unique_ptr
, Иногда даже необходимо получить необработанный указатель, чтобы взаимодействовать с API, для которого требуются необработанные указатели.
Более того, собственность не об указателях. Box
/unique_ptr
позволяет распределять объект в куче, но это ничего не меняет в отношении владения, времени жизни и т. д. по сравнению с размещением того же объекта в стеке (или внутри другого объекта, или в любом другом месте). Чтобы получить такой же пробег от такой системы в C ++, вам нужно было бы создать такие обертки с «подсчетом заимствований» для всех объектов везде, а не только для unique_ptr
s. И это довольно непрактично.
Итак, давайте вернемся к опции времени компиляции. Компилятор C ++ не может помочь нам, но может быть, линты могут? Теоретически, если вы реализуете часть системы типов на протяжении всего жизненного цикла и добавляете аннотации ко всем используемым вами API (в дополнение к вашему собственному коду), это может сработать.
Но это требует аннотации для всех функций, используемых во всей программе. Включая частную вспомогательную функцию сторонних библиотек. И те, для которых нет исходного кода. И для тех, чья реализация слишком сложна для понимания linter (из опыта Rust, иногда причина, по которой что-то безопасно, слишком тонкая, чтобы выразить ее в статической модели времени жизни, и она должна быть написана немного по-другому, чтобы помочь компилятору). Последние два года линтер не может проверить, что аннотация действительно верна, поэтому вы вернулись к доверию программиста. Кроме того, некоторые API (или, точнее, условия, когда они безопасны) не могут быть действительно хорошо выражены в целой системе, поскольку Rust использует ее.
Другими словами, полный и практически полезный линтер для этого был бы существенным оригинальным исследованием с связанным риском неудачи.
Может быть, есть промежуточный пункт, который получает 80% выгод с 20% стоимости, но, поскольку вы хотите получить твердую гарантию (и, честно говоря, я бы тоже этого хотел), неудача. Существующие «хорошие практики» в C ++ уже имеют большое значение для минимизации рисков, по сути продумывая (и документируя), как это делает программист Rust, без помощи компилятора. Я не уверен, есть ли много улучшений по сравнению с этим, учитывая состояние C ++ и его экосистемы.
tl; dr Просто используйте Rust 😉
Я верю, что вы можете получить немного о преимуществах Rust, применяя некоторые строгие соглашения о кодировании (в конце концов, это то, что вы должны были бы сделать, так как с помощью «магии шаблона» нет способа сообщить компилятору не скомпилировать код, который не используйте сказанное «волшебство»). С моей головы, следующее могло бы тебя … ну …вид закрыть, но только для однопоточных приложений:
new
непосредственно; вместо этого используйте make_unique
, Это частично способствует обеспечению того, что объекты, выделенные в куче, «принадлежат» в стиле Rust.&&
) и / или R-значения ссылаются на unique_ptr
s.К сожалению, я не могу придумать способ реализовать правило Руста о том, что изменяемые ссылки могут существовать только где-нибудь в системе когда есть нет другие существующие ссылки.
Кроме того, для любого вида параллелизма вам нужно будет начать работать со временем жизни, и единственный способ, которым я могу придумать, чтобы разрешить межпотоковое управление временем жизни (или межпроцессное управление временем жизни с использованием разделяемой памяти), — это реализовать свою собственную ». ptr-with-life «обертка. Это может быть реализовано с использованием shared_ptr
потому что здесь подсчет ссылок был бы действительно важен; Это все еще немного ненужные издержки, потому что блоки подсчета ссылок на самом деле имеют два счетчики ссылок (один на все shared_ptr
указывает на объект, другой для всех weak_ptr
с). Это тоже немного … странный, потому что в shared_ptr
сценарий, все с shared_ptr
имеет «равное» владение, тогда как в сценарии «заимствование на весь срок службы» только один поток / процесс должен фактически «владеть» памятью.
Вы можете использовать расширенную версию unique_ptr
(для обеспечения уникального владельца) вместе с расширенной версией observer_ptr
(чтобы получить хорошее исключение во время выполнения для висячих указателей, т.е. если исходный объект поддерживается через unique_ptr
вышел за рамки). Trilinos пакет реализует этот расширенный observer_ptr
они называют это Ptr
, Я реализовал расширенную версию unique_ptr
здесь (я называю это UniquePtr
): https://github.com/certik/trilinos/pull/1
Наконец, если вы хотите, чтобы объект был размещен в стеке, но при этом вы могли передавать безопасные ссылки, вам нужно использовать Viewable
класс, посмотрите мою первоначальную реализацию здесь: https://github.com/certik/trilinos/pull/2
Это должно позволить вам использовать C ++ точно так же, как Rust для указателей, за исключением того, что в Rust вы получаете ошибку времени компиляции, в то время как в C ++ вы получаете исключение времени выполнения. Также следует отметить, что вы получаете исключение только во время выполнения в режиме отладки. В режиме Release классы не выполняют этих проверок, поэтому они работают так же быстро, как и в Rust (по существу, так же быстро, как необработанные указатели), но затем они могут стать причиной ошибки. Поэтому необходимо убедиться, что весь набор тестов работает в режиме отладки.