31 мар. 2007 г.

Кодогенерация при помощи Boost.Preprocessor

Boost.Preprocessor — это хорошо. Особенно когда нужно быстро нагенерировать код без привлечения дополнительных инструментов типа Perl'а.

Плюсы:
  • Сам факт того, что применяется кодогенерация позволяет убрать дублирование. За счет этого снижается вероятность возникновения ошибок, сокращается ручная работа, облегчается рефакторинг и повышается читаемость кода. Кодогенерация позволяет убрать из конечного приложения часть «архитектурного оверхеда» — сдвинуть баланс эффективность-поддерживаемость кода в сторону эффективности.
  • Генерация кода средствами того же языка, на котором написан остальной код. Меньше объем технологий, которые требуется знать для поддержки программы. Считаем, что средний плюсовик лучше знает препроцессор чем тот же Perl.
  • Отсюда же меньший набор программных средств, привлекаемых для сборки. Для получения исполнимого файла не нужно ничего кроме стандартных средств сборки программ, написанных на C++.
  • По сравнению с кодогенерацией на шаблонах ощутимо выше скорость сборки (практически мгновенно), ощутимо меньше объем генерируемого кода.
  • Boost.Preprocessor поддерживается "AS IS" гораздо большим числом компиляторов чем современные техники программирования с использованием шаблонов.
На тему объема и скорости моя любимая страшилка — активно использующий шаблоны проект в котором полная пересборка занимала порядка часа, с генерацией порядка шести гигабайт промежуточных файлов, компилирующихся в пятнадцатимегабайтный экзешник... Да, каюсь, я был основной причиной появления этого монстра. С тех пор бездумное увлечение шаблонами как-то прошло. Но об этом в другой раз.

Так вот. Там где плюсы, там, очевидно, есть и минусы:
  • Генерация кода при помощи макросов препроцессора — дело, совсем не похожее на работу с обычным императивным языком. Это, фактически, функциональное программирование. На препроцессоре можно сделать очень многое (фактически, он Turing-complete, так что, формально, можно сделать вообще все что угодно), но сложные задачи на нем решаются совсем не так, как решались бы на C++ или Perl'е.
  • Страдает отлаживаемость кода как во время компиляции, так и во время выполнения — и компилятор и отладчик вместо того, чтобы показывать реальное место ошибки в коде, указывают на строку, в которой раскрылся макрос.
Классический аргумент макросоненавистников про засорение макросами пространства имен мы отметаем — просто нужно соблюдать единообразные правила именования макросов (уникальные префиксы и т.п.) и делать #undef'ine сразу после их использования.

Если по поводу необходимости изучения работы с препроцессором возразить нечего, то с проблема с отлаживаемостью кода вполне решаема. Достаточно компилировать предварительно препроцессированный файл. Большинство компиляторов предоставляют средства для получения результатов работы препроцессора (gcc -E; cl /EP /P /C и т.п.). В конце концов есть Boost.Wave.

При компиляции предварительно сгенерированного файла можно будет иметь перед глазами реальный код на который ругается компилятор, или с которым идет работа в отладчике. Однако, результат, получаемый простым препроцессированием явно неудовлетворителен. Конечный файл будет содержать все подключенные к нему заголовки и, хуже того, тот код, что был сгенерирован макросами, скорее всего будет находиться на одной строке.

Эта проблема решается с минимальным объемом трудозатрат с использованием простой утилиты, умеющей, руководствуясь метками в конечном файле, комбинировать его с исходным (я пользовался специально написанным скриптом на Perl'е) и утилиты для автоматического форматирования кода типа indent или artistic style. При желании можно даже настроить автоматическое форматирование так, что сгенерированный код не будет выбиваться из общей стилистики, заданной принятыми в компании coding guidelines.

Для разметки конченого файла удобно использовать стандартные макросы __FILE__ и __LINE__. Общий подход примерно такой:
// codegeneration.h
#define MY_SKIP_BEGIN
#define MY_SKIP_END
#ifdef MY_CODEGENERATION
#define MY_GENERATE_BEGIN MY_BEGIN(__FILE__, __LINE__)
#define MY_GENERATE_END MY_END(__FILE__, __LINE__)
#else
#define MY_GENERATE_BEGIN
#define MY_GENERATE_END
#endif // MY_CODEGENERATION

// intermediate.cpp
#include <cstdio>
#include <boost/preprocessor.hpp>
#include "codegeneration.h"

MY_SKIP_BEGIN

#define MY_DATA (Boost)(Preprocessor)

#define MY_GENERATE_ITEM(s, data, elem) \
BOOST_PP_STRINGIZE(elem)

#define MY_GENERATE(data) \
BOOST_PP_SEQ_FOR_EACH(MY_GENERATE_ITEM, _, data)

MY_SKIP_END

int main()
{
printf(
MY_GENERATE_BEGIN // Line 20
MY_GENERATE(MY_DATA)
MY_GENERATE_END // Line 22
);
return 0;
}

MY_SKIP_BEGIN

#undef MY_DATA
#undef MY_GENERATE_ITEM
#undef MY_GENERATE

MY_SKIP_END

// generated.cpp, сгенерированный препроцессором файл
/* ...Содержимое заголовочных файлов... */

int main()
{
printf(
MY_GENERATE_BEGIN("source.cxx", 20)
"Boost""Preprocessor"
MY_GENERATE_END("source.cxx", 22)
);
return 0;
}
Написать скрипт, который из source.cxx и intermediate.cpp делает один файл — достаточно тривиальная задача. Грубо говоря, нужно заменить текст в конечном файле снаружи MY_GENERATE_END/_BEGIN на куски оригинального файла по указанным строкам, исключая из оригинального текста части, ограниченные MY_SKIP_BEGIN/_END. На результат нужно натравить утилиту автоматического форматирования кода.

Для GCC командная строка выглядит примерно так:
g++ -E -DMY_CODEGENERATION $CFLAGS -include boost/preprocessor.hpp -x c++ source.cxx | ./pphandler.pl source.cxx - - | indent > out.cpp
Нужно явным образом указывать язык, поскольку расширение .cxx вызывает сложности у gcc. Имя исходного файла передается скрипту явным образом для упрощения логики его работы. Временный файл intermediate.cpp здесь отсутствует, все общение между программами происходит через pipe'ы.

Конечный файл out.cpp, если соответствующим образом настроить indent, будет выглядить примерно так:

#include <cstdio>
#include <boost/preprocessor.hpp>
#include "codegeneration.h"

int main()
{
printf(
"Boost""Preprocessor"
);
return 0;
}

Дополнительное преимущество такого подхода в том, что предварительное препроцессирование не является обязательным. В отсутствие макроса MY_CODEGENERATION код собирается AS IS после простого копирования source.cxx в out.cpp. Опыт показывает, что временными затратами на такую многоступенчатую кодогенерацию, по крайней мере на файлах среднего размера, можно пренебречь. Даже без тюнинга автоматического форматирования кода конечный результат получается вполне пригодный для работы.

Для ознакомления с Boost.Preprocessor рекомендуется к изучению посвященный ему кусок книги «C++ Template Metaprogramming», находящийся в открытым доступе: «Appendix A: An Introduction to Preprocessor Metaprogramming».

Комментариев нет: