C++ / [Из песочницы] Компилирование заголовочных файлов или документация на халяву

в 16:50, , рубрики: c++, doxygen, метки: ,

Для кого эта статья

Вряд ли опытные C++ разработчики найдут что-нибудь новое и интересное в этой статье. Максимум банальную команду
gcc -c -x c++ -I ./ */*/*/*/*.h
которую они и так знают.
А вот если Вы — разработчик начинающий, или только в первый раз строите документацию по своему проекту, или пробовали это однажды, но увидев тот бред, что сгенерировал doxygen, удалили его и забыли как страшный сон, добро пожаловать под кат, скорее всего Вы найдете дальше парочку полезных мыслей.

Введение

Программирование на языке C++ — это в первую очередь анализ существующего кода.
Вот уже несколько лет я принимаю участие в разработке системы имитационного моделирования. Разработка ведется на великом и могучем… детище Страуструпа. И, надо сказать, сей проект уже давно пишется не столько на C++, сколько на собственном диалекте из макросов (ниже станет понятно, почему это важно). Думаю ситуация такая знакома многим C++ девелоперам.

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

Предыстория

Время шло, мой профессионализм (как я надеюсь) рос, проект развивался и захотелось мне навести порядок в библиотеке, которую активно правил. Под порядком здесь подразумевается концепция один заголовочный файл = одна сущность (или несколько небольших и тесно связанных), а также разделение всех файлов на три типа — заголовочные, внутренние и исходными — соответственно, *.h, *.inl, *.cpp. Причем разделение должно пройти таким образом, чтобы в заголовочных файлах не осталось определения ни одной функции-члена класса, а все они были либо в cpp-файлах, либо в inl-файлах. Помимо этого весь код модуля должен был быть отформатирован по единому стандарту, и, что самое интересное, большинство сущностей должны были быть откомментированны с использованием специальных команд doxygen’а (interface, class, todo, brief, enum и прочие).

Проблема

Сказано — сделано. Спустя примерно несколько недель убитых вечеров безумная задача выполнена! Код красив так, что чуть слеза не наворачивается.
И настало время откинуться на спинку стула в ожидании, пока doxygen построит мне красивейшую и правильнейшую документацию с описанием моего (на тот момент уже самого любимого) модуля системы. И вот с замиранием сердца
cd Project/doc
doxygen project-doxyfile
cd html/
./index.html

Однако то, что предстало моему взору было, мягко говоря, фигней. Doxygen откровенно схалтурил: диаграммы неполные, namespace’ыпустые, макросы мне пытается за функции выдать, в общем всего не перечесть… Но первичный анализ показал, что он не понял макросы (все namespace’ы, умные указатели (собственного производства, кстати) и многое другое было задано с помощью макросов).

Решение в опции PREDEFINED?

Под подозрение в первую очередь попали настройки doxygen’а. Были перепроверены такие опции как ENABLE_PREPROCESSING, MACRO_EXPANSION, EXPAND_ONLY_PREDEF, SEARCH_INCLUDES и INCLUDE_PATH. Но настройки выглядели логично, а макросы правильно восприниматься не стали. Тогда в ход пошла опция PREDEFINED, хотя она и не отвечала на вопрос “почему doxygen лажает?”, но позволила ему объяснить нужные макросы. Это и стало решением проблемы на некоторое время. Но тот факт, что нужно было постоянно дописывать в эту опцию все новые макросы, весьма удручал и заставил продолжить изучение doxygen’а.

Проблема в коде!

Долго думал я над поведением doxygen’а и гуглил. А однажды даже слил его себе по svn с праведной мыслью найти и пофиксить страшный баг, мешающий ему обрабатывать макросы :)

Но вовремя остановился я, ибо понял в чем отличие его от компилятора. Цели у них весьма разные (и у доксигена в некотором роде даже сложнее), потому что ему надо не только понять cpp-файлы, но проделать тоже самое и с заголовочными файлами, чтобы потом красиво показать это все пользователю, т.е. по-файлово. И отсюда вытекает кое-что интересное, о чем я раньше не думал: компилятор не интересуется заголовочными файлами, на вход ему поступает “единица трансляции”, или выход препроцессора с уже отработанными директивами #include. Позволю себе перефразировать это таким образом — компирятор имеет дело с заголовочными файлами лишь косвенно, так сказать, в контексте единицы трансляции. И компилирует его в этом самом контексте. Отсюда следует неутешительный вывод — заголовочный файл может быть неправильным сам по себе, но становиться правильным в контексте своего использования, и компилятор ничего не скажет вам об этом!

А вот с doxygen’ом это не проходит — заголовочный файл анализируется как самостоятельный, самодостаточный документ с исходным кодом. И если в нем не хватает объявлений используемых сущностей (которые появляются в контексте использования данного заголовочного файла), то doxygen будет ошибаться. И очень похоже, что именно эта болезнь постигла наш проект.

Так какая же ошибка, неуловимая для компилятора, затаилась в заголовочном файле? Это недостоющии #include директивы тех файлов, где были определены пресловутые макросы. Т.е. при компиляции cpp-файлов все определения попадали в текущую единицу трансляции каким-нибудь обходным путем, а не через “проблемный” заголовочный файл. После этого открытия в нашей команде прошло совещание, с главным вопросом на повестке дня “и что с этим делать, собственно говоря?”. Вопрос был решен в пользу того, что подобное поведение — отсутствие всех нужных инклудов — ошибка. Основной довод весьма прост — заголовочный файл потенциально может быть включен куда угодно, и значит должен быть настолько самодостаточен, чтобы скомпилироваться.

Решение — “компиляции заголовочных файлов”

Вот тут-то и стал вопрос о “компиляции заголовочных файлов”.
Смысл этого мероприятия — заставить компилятор проанализировать все заголовочные файлы без внесения их в контекст исходных (*.cpp) файлов, и сообщить об ошибках. И тогда, если их исправить, то у doxygen’а не должно остаться никаких отмазок, чтобы правильно построить документацию по проекту со всеми диаграммами и прочим.

Теперь пришло время поговорить непосредственно о компиляторе, используемом в нашей команде.
По историческим причинам это ms visual studio, стандарта аж 2008-го года. Но буквально перед финалом данной истории успешно завершился проект перевода вычислительного ядра системы под GNU/Linux. И естественным образом в данной среде был выбран в качестве компилятора GCC, версии 4.6.
И начал терзать его просьбами я, мол скомпилируй мне заголовки. И долго сопротивлялся он… пока не прогнулся.
Вдумчивое чтение его man’а указало мне опцию -I, с помощью которой GCC можно указать путь, от которого стоит искать инклуды в кавычках.
Здесь же следует отметить один немаловажный факт, в проекте своем мы уже давно (и весьма успешно) придерживаемся правила “указывай путь к файлу в инклуде от корня проекта”. Эта дисциплинированность позволила обойтись единственной опцией -I.
Далее осталось подать на вход gcc все заголовочные файлы проекта. Но здесь вышла некоторая заминка связанная с нежеланием gcc принимать на вход выход команды ls через конвейер. Но решение оказалось еще тривиальнее, gcc и так осуществлял поиск входных файлов по переданной маске.
Таким образом, команда
g++ -I ./ */*/*/*/*.h */*/*/*.h */*/*.h
полностью осуществляет валидацию всех заголовочных файлов библиотеки.
Следующий раз gcc начал сопротивляться, если ему дать на вход *.inl файлы. Помогли опции -c (только компиляция без линковки), и -x c++ (явное задания языка программирования).
Итоговая команда, используемая для валидации заголовочных (*.inl файлы мы решили оставить в покое) файлов:
gcc -c -x c++ -I ./ */*/*/*/*.h */*/*/*.h */*/*.h

Ну что ж, осталось исправить ошибки и пользоваться doxygen’ом.

И, в общем-то, еще, наверняка, можно кое-что улучшить, например, прикрутить сюда cmake, но основная мысль данного поста именно в необходимости “компиляции заголовочных файлов” для борьбы с, казалось бы, странным поведением doxygen’а. Так что на этом остановлюсь, пока не утомил последнего самого усидчивого читателя ;)

Вместо выводов

И собственно, почему пост называется “… или документация на халяву”.
Да просто в коде может не быть ни одной строчки doxygen’овского комментария, но все равно можно построить по нему документацию со всевозможными диаграммами, что может очень помочь при изучении нового проекта. А именно автоматическое построение документации (с удобной навигацией и диаграммами) должно было облегчить новым студентам (а все это происходит в техническом ВУЗе) изучение весьма большой и сложной системы и являлось первоначальной целью использования doxygen в нашем проекте.

Естественно, в комментариях жду конструктивную критику, исправления, дополнения моих в меру кривых команд и вопросы.

Автор: pixelshader

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js