После появления в стандартной библиотеке С++ умных указателей, проблема управления временем жизни объекта была решена. Можно создавать объекты на стеке, тогда они автоматичести удалятся при выходе из области видимости, либо использовать unique_ptr для создания объектов с экслюзивным владением или shared_ptr для совместного владения. Но только для shared_ptr в стандартной библиотеке существует невладеющий указатель weak_ptr, который предотвращает использование невалидного указателя. Для остальных случаев используют «старые и опасные» raw pointers.
Как же предлагают решить эту проблему разработчики языка?
На сайте CppCoreGuidelines есть несколько любопытных документов (здесь и здесь).
Основной посыл таков: мне не можем реализовать безопасные невладеющие указатели, не нарушая zero overhead principle. Поэтому мы не будем реализовывать такие средства в runtime, а постараемся решить проблему статическим анализом кода.
Я не согласен с такой позицией и вот мои доводы:
- Статический анализ не сможет отловить все проблемы. Сами авторы статей говорят о том, что иногда прийдется помечать некоторые конструкции как безопасные, оставляя вопрос корректности кода на совести разработчика.
- Для того, что бы статический анализ заработал, нужна поддержка со стороны компиляторов. Когда она появится — неизвестно.
- Статический анализ не позволяет иметь слабые невладеющие указатели (аналог weak_ptr, который зануляется при удалении объекта).
- Реализация таких указателей нарушает zero overhead principle — это правда. Но, на мой взгляд, это принцип также нарушают shared_ptr, т.к. имеет дополнительный ref_count объект или, например string с его small string optimization. В любом случае, стандартная библиотека предоставляет классы с четко описанными характеристиками, а программист решает подходят эти классы для него или нет.
По моему мнению, стандартная библиотека должна предоставлять классы для всех основных сценариев программирования. Невладеющие указатели и доступ по висячим ссылкам большая и до сих пор незакрытая тема в С++. Думаю стандартная библиотека должна предоставлять программисту опциональную возможность иметь такие указатели.
В своем проекте RSL я попробовал реализовать такой указатель. Основная идея не нова: необходим объект, который при разрушении будет нотифицировать указатели о факте удаления.
Таким образом мы имеем два класса:
- rsl::track::trackable — класс, который оповещает указатели при удалении.
- rsl::track::pointer — собственно невладеющий указатель.
Когда rsl::track::pointer'ы указывают на один и тот же rsl::track::trackable объект, они выстраиваются в двухсвязный список. Указатель на голову списка содержится в rsl::track::trackable. Таким образом, создание указателей занимает константное время. Размер rsl::track::trackable составляет один указатель, а rsl::track::pointer — 4 указателя (указатель на объект, два указателя для организации списка и еще один для реализации полиморфного поведения). Возможно более оптимальная организация указателей, если кто знает, прошу рассказать.
Так же данная реализация не потоко безопасна, для обеспечения работы в разных потоках прийдется добавлять std::atomic_flag и замедлять модификацию указателей.
Кроме того, с появлением allocator aware containers, появилась возможность реализовать аллокатор, который позволяет использовать rsl::track::pointer со стандартными контейнерами. Основная идея в том, что теперь все аллокации в контейнерах делаются экземпляром аллокатора, хранящегося в контейнере, или его шаблонной копии, и мы можем хранить rsl::track::trackable в аллокаторе и передавать его в копии.
В тестах приведены примеры работы с основными стандартными контейнерами, включая std::array, а также unique_ptr.
В заключении хочу привести еще один сценарий, в котором rsl::track::pointer будут полезны. Это ситуации, аналогичные delete this. Обычно такое происходит коственно при вызове врешнего по отношению к объекту кода, функтора или сигнала. Такого рода ошибки редки, но трудно уловимы.
Для таких случаев (да и любых других проблем с доступом по висячей ссылке) используют такие средства как google sanitizers, которые позволяют отлавливать подобные проблемы.
Но эти средства имеют свои недостатки:
- Работают не везде (не на всех платформах).
- Необходима инструментализация кода — код отличается от продакшена.
- Нет аналога weak_ptr, указателя котроый зануляется при удалении объекта.
- Детектирует время и место доступа по невалидному указателю. Хотя, как правило, более интересен момент, когда происходит удаление объекта, на который есть указатели. По этой же причине инструментация детектирует не все случаи неправильного доступа, а лишь те, которые реально отработали при тестировании.
Надеюсь библиотека окажется полезной С++ разработчикам. Возможно есть другие способы решения подобных проблем, с удовольствием выслушаю в комментариях.
P.S. Еще раз, для удобства, привожу ссылку на код библиотеки RSL.
Автор: lexxmark