Программам необходимы данные. Программы выдают лишь настолько хороший результат работы, насколько полны и валидны были входные данные. Для некоторых программ входными данными являются обычные файлы или полученная из сети информация (пример — ваш браузер). Другие программы оперируют исходными кодами. Эти вторые «мета-программы» тоже нуждаются в данных. Чем они будут качественнее — тем качественнее будет и результат.
Что же за данные мы «скармливаем» таким программам? Ну, в С++ более важным чем «что?» является вопрос «когда?» (помните Морфеуса?). Программа на С++ это всего-лишь последовательность битов, которые компилятор пытается прочитать и понять. И вот в процессе этого «понимания» компилятор преобразует код на С++ в машинные инструкции и (что особенно интересно!) выполняет некоторую часть кода вашей программы. Да, мы говорим о мета-программировании на этапе компиляции.
Возвращаемся к вопросу «что?». Мы хотим иметь доступ ко всем сущностям, которые только теоретически могут быть доступны на этапе компиляции: типы, члены классов, функции, аргументы, пространства имён, номера строк кода, имена файлов — и всё это хорошо бы иметь в «чистом виде», без каких-либо странных препроцессорных хаков или использования сторонних утилит. Кроме того, хорошо бы получить и менее очевидные вещи: информацию о конвертируемости одних типов в другие, отношениям наследования и агрегации, дружественных классах и функциях и т.д.
Компилятор языка С++ уже имеет всю эту информацию! Но, к сожалению, не в доступной для мета-программирования форме. Получается странная такая ситуация — мы можем выполнить кое-какой код на этапе компиляции — но вот большинства данных у нас нет. Ну и здесь было бы логично задаться вопросом «А как же мы можем эти данные получить?».
Идея лежит на поверхности и многие, наверняка, успешно применяли её ранее: сделать вышеуказанные данные доступные мета-программам без требования какой-либо информации от компилятора.
Давайте посмотрим, кто у нас вообще есть. Первый — компилятор. Вторая — метра-программа и последний (по порядку, но не по важности) — это программист. Ну поскольку машины пока ещё не захватили мир и большинство программ на сегодняшний день пишется всё-же людьми.
Данные этапа компиляции должны быть видимы и понятны всем трём из них. На сегодняшний день программисты на С++, невзирая на боль, пишут код в форме, более удобной компилятору и метапрограмме. Основными примерам являются идиома traits, библиотека type_traits и иногда — специальные кодогенераторы, которые парсят код на С++ и «вынимают» взаимоотношения классов. Например, скрипт gen-meta.py из LEESA генерирует списки типов (векторы Boost MPL) для классов, которые содержат в себе другие классы.
Когда код не автогенерируемый, мы делаем его приятным для программиста используя макросы. Многие не любят макросы из-за того, что они «прячут» код и данные, которые в сущности за ними стоят, но сейчас не об этом. Есть много примеров действительно мощных макросов: Boost SIMD, Boost MPL (когда ещё не было variadic-шаблонов), «умные» перечисления, да и многое другое. Когда макросы используются по-умному — они действительно выглядят как магия. Мне довелось почувствовать это при работе над библиотекой RefleX.
RefleX — это средство моделирования типов с помощью рефлексии на этапе компиляции для DDS Topics. Оно открытое, но вам понадобиться RTI Connext DDS, чтобы попробовать RefleX «руками». По ходу его работы происходит преобразование нативного С\С++ типа в его представление, называемое TypeObject и маршалинг ваших данных в то, что называется DynamicData-объект. Обратите внимание, что и TypeObject и DynamicData — сериализуемы, поскольку в реальном коде частенько бывает нужно сохранить данные на диск или передать по сети.
Вот пример:
Макрос RTI_ADAPT_STRUCT разворачивается в примерно 120 строк С++ кода, содержащего информацию о ShapeType, которая может быть использована на этапе компиляции. Он основан на макросе BOOST_FUSION_ADAPT_STRUCT. Этот макрос открывает внутренности определённого типа для библиотеки RefleX. Мета-программы на RefleX используют эти данные для своих целей. Информация включает типы членов класса, их имена, перечисления и другую информацию.
Последние две open-source библиотеки я написал с использованием этого механизма: в одной «данные» генерировались питоновским скриптом, в другой — макросами типа вышеописанных. Эти библиотеки получились очень мощными.
Естественным шагом эволюции данной парадигмы должна стать её поддержка на уровне стандарта языка. Если что-то даёт столь существенные преимущества — язык и компилятор должны брать на себя труд по предоставлению необходимого функционала.
Всё это приводит нас к главной теме этой статьи: рефлексии на этапе компиляции. Она нужна нам. Это необходимый и правильный шаг эволюции языка С++. Когда она будет доступна — все эти макросы и кодогенераторы будут не нужны. У нас просто будет вся необходимая информация, там где она необходима и тогда, когда она необходима. Всё это будет работать быстрее, выглядеть проще и вообще быть просто сногсшибательной фичей.
Стандарт С++1y обещает быть очень интересным, будем следить за новостями.
Что же за данные мы «скармливаем» таким программам? Ну, в С++ более важным чем «что?» является вопрос «когда?» (помните Морфеуса?). Программа на С++ это всего-лишь последовательность битов, которые компилятор пытается прочитать и понять. И вот в процессе этого «понимания» компилятор преобразует код на С++ в машинные инструкции и (что особенно интересно!) выполняет некоторую часть кода вашей программы. Да, мы говорим о мета-программировании на этапе компиляции.
Возвращаемся к вопросу «что?». Мы хотим иметь доступ ко всем сущностям, которые только теоретически могут быть доступны на этапе компиляции: типы, члены классов, функции, аргументы, пространства имён, номера строк кода, имена файлов — и всё это хорошо бы иметь в «чистом виде», без каких-либо странных препроцессорных хаков или использования сторонних утилит. Кроме того, хорошо бы получить и менее очевидные вещи: информацию о конвертируемости одних типов в другие, отношениям наследования и агрегации, дружественных классах и функциях и т.д.
Компилятор языка С++ уже имеет всю эту информацию! Но, к сожалению, не в доступной для мета-программирования форме. Получается странная такая ситуация — мы можем выполнить кое-какой код на этапе компиляции — но вот большинства данных у нас нет. Ну и здесь было бы логично задаться вопросом «А как же мы можем эти данные получить?».
Идея лежит на поверхности и многие, наверняка, успешно применяли её ранее: сделать вышеуказанные данные доступные мета-программам без требования какой-либо информации от компилятора.
Давайте посмотрим, кто у нас вообще есть. Первый — компилятор. Вторая — метра-программа и последний (по порядку, но не по важности) — это программист. Ну поскольку машины пока ещё не захватили мир и большинство программ на сегодняшний день пишется всё-же людьми.
Данные этапа компиляции должны быть видимы и понятны всем трём из них. На сегодняшний день программисты на С++, невзирая на боль, пишут код в форме, более удобной компилятору и метапрограмме. Основными примерам являются идиома traits, библиотека type_traits и иногда — специальные кодогенераторы, которые парсят код на С++ и «вынимают» взаимоотношения классов. Например, скрипт gen-meta.py из LEESA генерирует списки типов (векторы Boost MPL) для классов, которые содержат в себе другие классы.
Когда код не автогенерируемый, мы делаем его приятным для программиста используя макросы. Многие не любят макросы из-за того, что они «прячут» код и данные, которые в сущности за ними стоят, но сейчас не об этом. Есть много примеров действительно мощных макросов: Boost SIMD, Boost MPL (когда ещё не было variadic-шаблонов), «умные» перечисления, да и многое другое. Когда макросы используются по-умному — они действительно выглядят как магия. Мне довелось почувствовать это при работе над библиотекой RefleX.
RefleX — это средство моделирования типов с помощью рефлексии на этапе компиляции для DDS Topics. Оно открытое, но вам понадобиться RTI Connext DDS, чтобы попробовать RefleX «руками». По ходу его работы происходит преобразование нативного С\С++ типа в его представление, называемое TypeObject и маршалинг ваших данных в то, что называется DynamicData-объект. Обратите внимание, что и TypeObject и DynamicData — сериализуемы, поскольку в реальном коде частенько бывает нужно сохранить данные на диск или передать по сети.
Вот пример:
// shape.h
struct ShapeType
{
std::string color;
int x;
int y;
unsigned shapesize;
};
RTI_ADAPT_STRUCT(
ShapeType,
(std::string, color, KEY)
(int, x)
(int, y)
(unsigned, shapesize))
Макрос RTI_ADAPT_STRUCT разворачивается в примерно 120 строк С++ кода, содержащего информацию о ShapeType, которая может быть использована на этапе компиляции. Он основан на макросе BOOST_FUSION_ADAPT_STRUCT. Этот макрос открывает внутренности определённого типа для библиотеки RefleX. Мета-программы на RefleX используют эти данные для своих целей. Информация включает типы членов класса, их имена, перечисления и другую информацию.
Последние две open-source библиотеки я написал с использованием этого механизма: в одной «данные» генерировались питоновским скриптом, в другой — макросами типа вышеописанных. Эти библиотеки получились очень мощными.
Естественным шагом эволюции данной парадигмы должна стать её поддержка на уровне стандарта языка. Если что-то даёт столь существенные преимущества — язык и компилятор должны брать на себя труд по предоставлению необходимого функционала.
Всё это приводит нас к главной теме этой статьи: рефлексии на этапе компиляции. Она нужна нам. Это необходимый и правильный шаг эволюции языка С++. Когда она будет доступна — все эти макросы и кодогенераторы будут не нужны. У нас просто будет вся необходимая информация, там где она необходима и тогда, когда она необходима. Всё это будет работать быстрее, выглядеть проще и вообще быть просто сногсшибательной фичей.
Стандарт С++1y обещает быть очень интересным, будем следить за новостями.