Разбираясь с некоторыми вопросами высоконадежных систем, предназначенных для управления критически важных процессов информатизации (safety-critical systems), нашел некоторую особенность и попытался её разгрызть, почему сделано так.
Согласно стандарту ISO 61508 (сейчас он же действующий ГОСТ Р МЭК 61508), крайне не рекомендуется использовать динамическую память и динамические объекты (что для серьезных западных систем означает нельзя). Аналогичные требования есть в JPL Coding Standard (это ответственные системы NASA, например софт для марсоходов и МКС) и в MISRA C/C++(ответственные системы по умолчанию, например химическое производство которое может устроить катастрофу или автомобильный транспорт с тем же потенциалом для последствий).
Однако, если создать свою структуру данных и верифицировать её в рамках проекта (это обычно не означает написать пару сотен строк кода за день-два, а работу нескольких высококлассных специалистов в течение нескольких месяцев (счет для самых простых структур данных начинается с 3-х месяцев), которые проведут её через все этапы разработки с многоверсионными проверками спецификаций, полным черно-белым тестированием, верификацией формальными методами с использованием дедуктивного анализа и модели с задействованием автоматики, экспертная оценка как человеком, так и автоматикой (статическими анализаторами кода в т.ч.), и все это умножить на два из-за проверки независимой группой по всем этапам). Удовольствие дорогое, но возможное. (отдельным путем является использование компилятора, обеспечивающего безопасное поведение в случае отказов; классические и всем известные C/C++ к таковым не относятся; к ним обычно относится верифицированный куцый C/C++).
Авторов всего этого понять можно: динамика поставила на колени далеко не один проект С/C++, а в последние годы много усилий ушло на уменьшение последствий от возможных проблем на данном минном поле.
Прошло какое-то время, прежде чем мною ощутилась разница между описанным первым (глобальной динамикой) и вторым (верифицированными структурами данных): отдельно разрабатываемые структуры данных играют (в том числе) роль специализированных менеджеров памяти, а поставляемые в коробке new/delete и malloc/free — синглтонов языка программирования.
Динамика как синглтон
new/malloc и delete/free работают глобально, в одной большой куче. Доступ к ней есть отовсюду. О том, как лучше работать с этой кучей — изобретается множество инструментов и техник, так как надо контролировать создание (только один раз) и удаление (только один раз и только после создания), обрабатывать ошибки (переполнение памяти), попадать куда надо указателем (неопределенное поведение) и типом (срезка), получать доступ и разделять ответственность. Фактически сама куча является глобальной структурой данных, поддерживаемой на уровне языка.
Если мы переключаемся с фокуса глобального хранения на малую изолированную структуру данных, то сразу посредством модульности снимается комплекс проблем, в основном за счёт резкого снижения сложности и разделения ответственности.
Если мы берем тот же вектор, то он сам является динамической структурой и может что-нибудь сломать из-за недостатка памяти. Однако намного легче построить программу так, что вектор никогда не расширится выше какого-то предела и при этом не будет протекать (а он может). В случае же проблем можно реализовать переход в безопасное состояние, что делает систему более надежной и безопасной.
Синглтоны C++
Прошелся по языку и нашел следующие синглтоны:
- new/delete
- static
- cout/cerr/cin
- Ресурсы (файловые дескрипторы, коннекты, потоки, …).
- Нелокальные макросы.
- Указатели.
Т.о. язык предоставляет пользователю набор синглтонов в упаковке, готовых к использованию.
Свойства
Свойства очень похожи из того, что есть в оригинальном синглтоне. Для всех описанных (кроме макросов, они на этапе сборки):
- необходимо заниматься синхронизацией в многопоточном приложении;
- все сущности видны отовсюду, и любой может сломать сразу все;
- если в проекте/библиотеке сущность уже прибита гвоздями к стенке, то её размножить сложно;
- нужно заботиться о потенциальных побочных эффектах и сводить в stateless после каждого вызова;
- узкое горлышко многопоточной производительности;
- некоторые синглтоны обладают разной степенью глобальности.
Если начинают появляться проблемы с синглтоновостью, то часть из них решается разбиением на модули в виде отдельного процесса, у которого своя куча, свой лог, свои ресурсы. Таким образом мы синглтоны режем на другие синглтоны и частично решаем проблему модульностью. Вторая часть решается переходом на отдельные структуры данных. Т.е. из динамики лучше уйти в свои контейнеры, а если возможно — то в контейнеры проверенных временем библиотек (STL). Третья часть — если есть возможность уйти от динамики, то лучше уйти. Например полиморфить через ссылки, а не указатели.
Т.о., понимание синглтонов позволяет лучше ощутить источник проблем и ведет к светлому будущему (;