В контексте использования директивы
#include
есть два важных принципа.
Важный из этих двух сейчас - это принцип
Include What You Use.
Суть сводится к тому, что файлы исходного кода формирующие модуль трансляции должны в правильном порядке подключать все заголовки, объявления которых используют. Транзитивные включения не считаются. Методология соответствия IWYU включает в себя и правило перечисления подключаемых заголовков - от общих к частным. Именно таким образом должна обеспечиваться оптимальная скорость формирования модуля трансляции.
И с этим связано несколько проблем. Основные я опишу ниже.
Наивное следование IWYU в сложных проектах.
В обычном проекте считается что один файл исходного кода соответствует одному модулю трансляции. В более сложных проектах, где используется
SCU/
Unity Build, один модуль трансляции уже представлен несколькими файлами исходного кода. Но IWYU требует соблюдать правила только для модуля трансляции, а не для файлов исходного кода.
Эта ситуация обозначает одну из проблем соответствия IWYU, которую не замечают многие разработчики, кто пользуется IWYU и SCU одновременно. Дублирование директив, нарушение порядка от общего к частному и доступность директив для всего нижележащего кода других файлов исходного кода сводит на нет весь принцип IWYU.
Простейшей ошибкой наивного следования IWYU при использовании SCU является простой забытый инклуд, который присутствует в другом файле исходного кода, размещенном модуле трансляции выше по коду. SCU формируются динамически, на базе определенного алгоритма. Сегодня ошибки нет, а завтра исходный код попадет в другой модуль трансляции, где выше по коду уже не будет нужного инклуда и трансляция пойдет прахом. Особой пикантности данной ситуации добавляют разные сценарии SCU для разработчиков и систем CI. В этом случае ошибку с потерянным инкудом искать будут очень долго и не в одно лицо. Такие ситуации реально существовали у меня на работе. Особую пикантность такой ситуации добавляет и то, что на этой работе люди особо активно пропагандировали IWYU, говоря о безмерной пользе принципа вместе с использованием SCU.
Решением же является понимание несовместимости использования SCU и IWYU в одном проекте.
Излишне детальное следование IWYU.
Принцип IWYU говорит включать только то, чем пользуешься. Что это означает в деталях?
Когда заголовочный файл использует какое-то объявление, он должен сперва подключить соответствующий заголовок. Когда исходный код использует какое-то объявление, он должен сперва подключить соответствующий заголовок.
Код не пишется в вакууме, код пишется в составе библиотек и систем, в рамках которых код связывается между собой. На более мелком уровне код пишется в составе подсистем и модулей, а подсистемы и модули уже формируют более глобальные системы и библиотеки. В рамках всех этих единиц агломерации кода сам код является тесно связанным между собой. Заголовки одной подсистемы редко не будут перекрестно включать друг друга потому что активно пользуются их объявлениями для реализации функционала подсистемы.
Внешний же код, использующий функционал подсистемы, редко будет ограничиваться включением только одного заголовка, потому сами заголовки включают друг друга и через IWYU диктуют подобный подход своему пользователю.
Это заставляет блоки включений раздуваться до невиданных масштабов в сотни строк одних только #include
. Если проект использует #pragma once
, то огромные блоки включений сказываются только на удобстве чтения, сводя удобство разбора инклудов к нулю. Но если проект использует только define-guard
, то от огромных блоков инклудов начинает катастрофически страдать скорость сборки. В этот момент люди обычно вспоминают про SCU и с его помощью окончательно хоронят гибкость проекта в пучине ошибок совместного использования SCU и IWYU.
А решением в данном случае является выделение для единиц агломерации кода своих собственных публичных заголовков, внутри которых в правильном порядке будут подключены все заголовки используемого кода.
Этот простой шаг кратно сокращает списки инклудов в пользовательском коде, не противоречит IWYU и позволяет на более высоком уровне организовать публичный интерфейс модуля, подсистемы, системы или библиотеки.
Полагаясь на транзитивные включения, можно угодить в неприятности даже с кодом стандартной библиотеки. От стандарта к стандарту библиотечный код немного меняется и в любой момент может перестать подключать используемые в твоем коде заголовки. В этом случае при переходе на новый стандарт тебе придется сперва справиться с возникшими ошибками трансляции. Еще более остро эта проблема встает при обновлении сторонних библиотек, авторы которых, как правило, часто и кардинально меняют внутреннюю структуру библиотеки.
Соблюдение же принципа IWYU обещает реже встречаться с подобными проблемами и легче переживать обновление стороннего кода, делая тем самым проект более гибким и готовым к изменениям.