Pull to refresh

10 лет практики. Часть 2: ресурсы

C++
Здравствуйте. Я планировал написать большую статью об управлении ресурсами в С++.
Но на практике, тема эта такая сложная и многогранная, что я хочу остановиться на определённой методике, которой пользуюсь сам. Данная методика не является спасением на все случаи жизни, но экономит много времени и нервов при работе с объектами. При этом, не является широко известной.

Подход этот называется «everything belongs somewhere». О нём я впервые узнал, пересаживаясь c Qt на замечательный фреймворк U++, созданный группой авторов во главе с Миреком Фидлером (Mirek Fídler). Произошло это около пяти лет назад, так что я поделюсь не только самим методом, но и практическими советами исходя из опыта его применения.

Вкратце, суть методики можно описать фразой: «всё кому-то принадлежит».
Класс-хозяин несёт в себе все ресурсы, необходимые для полноценной работы. Например:
Объекты, описывающие узлы бункера, включаются в класс бункера. Объекты-помощники, а также объекты с данными для класса сложных расчётов, включаются в него. И так далее.
Исключение составят несколько глобальных переменных, представляющих собой самые основные объекты. В некотором роде, и они являются членами безымянного глобального «класса».

Теперь мы говорим следующее:
  1. объект определяется в виде обычного члена класса, либо помещается в контейнер, являющийся членом класса
  2. право владения объектом не передаётся

Заметьте: определяется не в виде указателя, не в виде ссылки, а обычным образом. Мы можем взять указатель на объект, передавать его куда-то, но только для использования. То есть сторонний код не может сделать delete или new нашему указателю. Им управляет только класс-хозяин.


Что делать, если объект будет создан не сразу?
В этом случае, понадобится либо одиночный контейнер (вроде unique_ptr), либо контейнер-массив. Главная их функция — автоматическое удаление объекта в деструкторе. Всё!

А вот здесь остановимся и подумаем, что же мы получили.

1. Мы избавились от ручных вызовов new/delete. Причём избавились так хорошо, что даже в случае выбросов исключения, а также любых других ситуаций, наши ресурсы будут гарантированно удалены.

2. Мы получили очень хорошую автоматическую систему управления ресурсами, учитывающую семантику программы.
Например, у нас есть несколько окон. Какое-то из окон по ходу работы программы уничтожается. При этом удаляются все его ресурсы. Мы точно контролируем момент удаления ресурса, это очень важно.
Мы не полагаемся на мусоросборщик. Мы точно знаем, что в нужный момент память будет освобождена.

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

3. Когда объект-хозяин выдаёт указатель на свой член, он может гарантировать, что по этому указателю существует нужный объект нужного класса. Когда мы пользуемся таким указателем, мы работаем в области видимости объекта-хозяина. А значит нам гарантируется валидность указателя. Понимаете? Время жизни объекта по указателю гарантируется самим компилятором, который на этапе компиляции проверит видимость объекта-хозяина.
Это же практически мечта: эффективно работать через указатели, валидность которых проверена ещё на этапе компиляции! И всё это — без накладных расходов.

4. Наконец, когда мы поняли, что у нас вопросы создания, удаления и валидности доступа решены без накладных расходов, мы приходим к тому, что «сложные» умные указатели со счётчиком ссылок и более сложными механизмами внутри, становятся попросту не нужны.

Мы накладываем ограничения на структуру программы. Проводим более тщательное проектирование и планирование. И за счёт этого, мы приходим к гораздо более детерминированной работе с ресурсами без накладных расходов. Отказываемся от всех явных вызовов delete, большинства явных вызовов new и контейнеров со счётчиками ссылок — и уходим от всех проблем, им сопутствующих.

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

Вот, если коротко, в чём заключается подход.

Теперь о подводных камнях.
Прежде всего, это может значить совершенно другой стиль декомпозиции на классы, по сравнению с мейнстримовым подходом, активно использующим умные указатели со счётчиком ссылок. Зачастую, наложить указанные ограничения очень непросто, и подход с умными указателями кажется более привлекательным. Но, как показывает практика разработчиков фреймворка и практика автора статьи — лучше постараться, и сделать всё в соответствии с подходом. Эти усилия в десятки раз окупятся при отладке и поддержке программы.

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

В-третьих, из этих правил может вытекать следующая стратегия работы с GUI. Контролы принадлежат их логическому владельцу (не окну, а тому, кто хранит в них свою семантику, ведь он её владелец). В этом смысле, у вас есть два выбора: апологеты MVC могут всё оставить как есть. Либо, вы можете воспользоваться таким подходом, который, как показывает практика, зачастую себя окупает. Разумеется, это не священная корова, а наоборот — подлежит критическому осмыслению и проверке на практике.

Пожалуйста, указывайте на неточности, пробуйте, критикуйте, применяйте. Буду рад любым откликам.

Литература:
1. U++ overview.

В следующей части предполагается обсудить передачу (pick beaviour) и перенос (moveable) объектов. Будет показано, как эти возможности дают существенный прирост скорости работы с объектами. А контейнеры на их основе в 4-5 раз быстрее STL.
После чего можно будет переходить к турбо-скоростной и очень безопасной реализации многопоточности, вдохновлённой Эрлангом.
Tags:ресурсыпамятьисключенияуказателиумные указателиобласть видимостивремя жизни
Hubs: C++
Total votes 30: ↑22 and ↓8 +14
Views3K

Comments 40

Only those users with full accounts are able to leave comments. Log in, please.
Разработчик C++
to 150,000 ₽НТЦ ПРОТЕЙСанкт-ПетербургRemote job
Senior C++ Developer
from 2,500 to 3,000 $Alex Staff AgencyRemote job
Senior C++ Engineer
to 230,000 ₽ItivitiСанкт-Петербург
C++ Toolset Developer [возможен Remote]
from 2,000 $Awem GamesRemote job
Разработчик C++ встраиваемые системы
from 180,000 to 250,000 ₽1 CEOСанкт-ПетербургRemote job