boost — Как включить парадигму владения Rust в Stack Overflow

Язык системного программирования Rust использует парадигму владения, чтобы обеспечить во время компиляции нулевую стоимость времени выполнения, когда ресурс должен быть освобожден (см. «Руст Книга о собственности»).

В C ++ мы обычно используем умные указатели для достижения той же цели — скрыть сложность управления распределением ресурсов. Хотя есть несколько отличий:

  • В Rust всегда есть только один владелец, тогда как C ++ shared_ptr может легко утратить владение.
  • В Rust мы можем заимствовать ссылки, которые нам не принадлежат, тогда как C ++ unique_ptr не может быть безопасным образом распространен с помощью weak_ptr и lock ().
  • Подсчет ссылок на shared_ptr стоит дорого.

Мой вопрос: как мы можем эмулировать парадигму владения в C ++ в следующих ограничениях:

  • Только один владелец в любое время
  • Возможность заимствовать указатель и использовать его временно, не опасаясь, что ресурс выйдет из области видимости (observer_ptr бесполезно для этого)
  • Как можно больше проверок во время компиляции.

Редактировать:
Учитывая комментарии, мы можем сделать вывод:

  • Никакой поддержки во время компиляции (я надеялся на некоторую неизвестную мне магию decltype / template) в компиляторах. Может быть возможно с помощью статического анализа в другом месте (заражение?)
  • Нет способа получить это без подсчета ссылок.
  • Нет стандартной реализации для разделения shared_ptrs с владением или заимствованием семантики
  • Можете свернуть свои собственные, создавая типы оберток вокруг shared_ptr и weak_ptr:

    • ведомый_птр: не копируемый, семантика перемещения, инкапсулирует shared_ptr, доступ к заимствованному_птр
    • loaned_ptr: копируемый, инкапсулирует слабый_ptr, метод блокировки
    • locked_ptr: не копируемый, семантика перемещения, инкапсулирует shared_ptr из блокировки

23

Решение

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

Что вы могли бы сделать, это иметь вариант unique_ptr это сохраняет счетчик количества «заимствований», активных во время выполнения. Вместо get() возвращая необработанный указатель, он возвращает умный указатель, который увеличивает этот счетчик при построении и уменьшает его при уничтожении. Если unique_ptr уничтожается, пока счетчик не равен нулю, по крайней мере, вы знаете, что кто-то где-то сделал что-то не так.

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

Более того, собственность не об указателях. Box/unique_ptr позволяет распределять объект в куче, но это ничего не меняет в отношении владения, времени жизни и т. д. по сравнению с размещением того же объекта в стеке (или внутри другого объекта, или в любом другом месте). Чтобы получить такой же пробег от такой системы в C ++, вам нужно было бы создать такие обертки с «подсчетом заимствований» для всех объектов везде, а не только для unique_ptrs. И это довольно непрактично.

Итак, давайте вернемся к опции времени компиляции. Компилятор C ++ не может помочь нам, но может быть, линты могут? Теоретически, если вы реализуете часть системы типов на протяжении всего жизненного цикла и добавляете аннотации ко всем используемым вами API (в дополнение к вашему собственному коду), это может сработать.

Но это требует аннотации для всех функций, используемых во всей программе. Включая частную вспомогательную функцию сторонних библиотек. И те, для которых нет исходного кода. И для тех, чья реализация слишком сложна для понимания linter (из опыта Rust, иногда причина, по которой что-то безопасно, слишком тонкая, чтобы выразить ее в статической модели времени жизни, и она должна быть написана немного по-другому, чтобы помочь компилятору). Последние два года линтер не может проверить, что аннотация действительно верна, поэтому вы вернулись к доверию программиста. Кроме того, некоторые API (или, точнее, условия, когда они безопасны) не могут быть действительно хорошо выражены в целой системе, поскольку Rust использует ее.

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

Может быть, есть промежуточный пункт, который получает 80% выгод с 20% стоимости, но, поскольку вы хотите получить твердую гарантию (и, честно говоря, я бы тоже этого хотел), неудача. Существующие «хорошие практики» в C ++ уже имеют большое значение для минимизации рисков, по сути продумывая (и документируя), как это делает программист Rust, без помощи компилятора. Я не уверен, есть ли много улучшений по сравнению с этим, учитывая состояние C ++ и его экосистемы.

tl; dr Просто используйте Rust 😉

22

Другие решения

Я верю, что вы можете получить немного о преимуществах Rust, применяя некоторые строгие соглашения о кодировании (в конце концов, это то, что вы должны были бы сделать, так как с помощью «магии шаблона» нет способа сообщить компилятору не скомпилировать код, который не используйте сказанное «волшебство»). С моей головы, следующее могло бы тебя … ну …вид закрыть, но только для однопоточных приложений:

  • Никогда не используйте new непосредственно; вместо этого используйте make_unique, Это частично способствует обеспечению того, что объекты, выделенные в куче, «принадлежат» в стиле Rust.
  • «Заимствование» всегда должно быть представлено через ссылочные параметры для вызовов функций. Функции, которые принимают ссылку должны никогда создать любой указатель на указанный объект. (В некоторых случаях может быть необходимо использовать необработанный указатель в качестве параметра вместо ссылки, но должно применяться то же правило.)
    • Обратите внимание, что это работает для объектов в стеке или же в куче; функция не должна заботиться
  • Перечислить собственность, конечно, представлена ​​через ссылки R-значения (&&) и / или R-значения ссылаются на unique_ptrs.

К сожалению, я не могу придумать способ реализовать правило Руста о том, что изменяемые ссылки могут существовать только где-нибудь в системе когда есть нет другие существующие ссылки.

Кроме того, для любого вида параллелизма вам нужно будет начать работать со временем жизни, и единственный способ, которым я могу придумать, чтобы разрешить межпотоковое управление временем жизни (или межпроцессное управление временем жизни с использованием разделяемой памяти), — это реализовать свою собственную ». ptr-with-life «обертка. Это может быть реализовано с использованием shared_ptrпотому что здесь подсчет ссылок был бы действительно важен; Это все еще немного ненужные издержки, потому что блоки подсчета ссылок на самом деле имеют два счетчики ссылок (один на все shared_ptrуказывает на объект, другой для всех weak_ptrс). Это тоже немного … странный, потому что в shared_ptr сценарий, все с shared_ptr имеет «равное» владение, тогда как в сценарии «заимствование на весь срок службы» только один поток / процесс должен фактически «владеть» памятью.

2

Вы можете использовать расширенную версию 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 (по существу, так же быстро, как необработанные указатели), но затем они могут стать причиной ошибки. Поэтому необходимо убедиться, что весь набор тестов работает в режиме отладки.

0
По вопросам рекламы [email protected]