Подтвердить что ты не робот

Как включить парадигму собственности на Rust в С++

Язык системного программирования Rust использует парадигму собственности для обеспечения во время компиляции нулевой стоимости для среды выполнения, когда ресурс должен быть освобожден (см. "Rust Book" Собственность").

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

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

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

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

Edit: С учетом комментариев до сих пор мы можем заключить:

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

    • owned_ptr: не копируемый, перемещение-семантика, инкапсуляция shared_ptr, доступ к заимствованному_ptr
    • заемный_ptr: копируемый, инкапсулирует weak_ptr, метод блокировки
    • locked_ptr: не копируемый, перемещение-семантика, инкапсулирует shared_ptr из блокировки weak_ptr
4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

Вы можете использовать расширенную версию unique_ptr (для принудительного применения уникального владельца) вместе с расширенной версией observer_ptr (чтобы получить отличное исключение во время выполнения для оборванных указателей, то есть, если исходный объект поддерживается через unique_ptr вышел из сферы действия). Пакет Trilinos реализует этот расширенный observer_ptr, они называют его Ptr. Я реализовал расширенную версию unique_ptr здесь (я называю это UniquePtr): https://github.com/certik/trilinos/pull/1

Наконец, если вы хотите, чтобы объект был выделен стекми, но все же можно передавать безопасные ссылки, вам нужно использовать класс Viewable, см. мою первоначальную реализацию здесь: <а2 >

Это должно позволить вам использовать С++ так же, как Rust для указателей, за исключением того, что в Rust вы получаете ошибку времени компиляции, а на С++ вы получаете исключение во время выполнения. Кроме того, следует отметить, что вы получаете только исключение во время выполнения в режиме отладки. В режиме Release классы не выполняют эти проверки, поэтому они работают так же быстро, как и в Rust (по сути, так же быстро, как и исходные указатели), но затем они могут выполнять segfault. Поэтому нужно убедиться, что весь тестовый пакет работает в режиме отладки.