Страницы

Поиск по вопросам

среда, 11 декабря 2019 г.

Автогенерация классов в C++

#cpp #препроцессор


Имеется структура данных, подобная следующей:

Group_1
    Type_1_1 Variable_1_1
    . . .
    Type_1_N Variable_1_N
    Group_1_1 Name_1_1
        Type_1_1_1  Variable_1_1_1
        . . .
        Type_1_1_N1 Variable_1_1_N1
    . . .
    Group_1_M Name_1_M
        Type_1_M_1  Variable_1_M_1
        . . .
        Type_1_M_N2 Variable_1_M_N2


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

Для указанной структуры необходимо построить C++-код следующего вида:

class Group_1_1 {
    public :
        Type_1_1_1  Variable_1_1_1;
        // . . .
        Type_1_1_N1 Variable_1_1_N1;
        void f(Worker& worker) {
            worker.begin("Group_1_1");
            worker.work("Variable_1_1_1",  Variable_1_1_1);
            // . . .
            worker.work("Variable_1_1_N1", Variable_1_1_N1);
            worker.end();
        }
}
// . . .

class Group_1 {
    public :
        Type_1_1 Variable_1_1;
        // . . .
        Type_1_N Variable_1_N;
        Group_1_1 Name_1_1;
        // . . .
        Group_1_M Name_1_M;
        void f(Worker& worker) {
            worker.begin("Group_1");
            worker.work("Variable_1_1",  Variable_1_1_1);
            // . . .
            worker.work("Variable_1_N", Variable_1_1_N1);
            Name_1_1.f(worker);
            // . . .
            Name_1_M.f(worker);
            worker.end();
        }
}


Здесь Worker - некоторый внешний класс.

Построить код необходимо без использования каких-либо внешних инструментов, можно
использовать только стандартные возможности компилятора и препроцессора. То есть можно
использовать макросы и шаблоны, но нельзя запускать какие-либо программы, не входящие
в состав компилятора. Можно при этом ограничиться стандартом c++11, совместимость с
предыдущими стандартами не требуется. Решение должно работать по меньшей мере со следующими
компиляторами: GCC 4.9+ (Linux), MinGW 4.9+ (Windows), MSVC 2015.

Собственно вопрос: можно ли это реализовать и если можно, то как?
    


Ответы

Ответ 1



Предлагаю решение с ипользованием boost::preprocessor. Пока что это решение только половины задачи, генерации отдельных классов. Входные данные должны быть представлены примерно так: #define GEN_GROUP 1 #define GEN_VAR 0 #define MY_STRUCT \ (GEN_GROUP, AType, A, (GEN_VAR, int, AInt) \ , (GEN_GROUP, BType, B, (GEN_VAR, int, BInt) \ , (GEN_VAR, float, BFloat) \ ) \ , (GEN_VAR, char, AChar) \ ) В понятиях boost::preprocessor это кортежи (tuple). А точнее кортежи которые содержат другие кортежи и таким образом образуют дерево. Струтура это дерева такая: элемент 0 - тип узла (группа или переменная), элемент 1 - тип данных, 2 - имя узла, и в случае группы остальные элементы начиная с 3 это поля или подгруппы: группа = (1, тип, имя, [переменная | группа]+ ) переменная = (0, тип, имя) Использовать эту структуру надо следующим образом, приведу просто код с коментариями: #include #define GEN_NODE_TYPE(node) BOOST_PP_TUPLE_ELEM(0, node) #define GEN_TYPE(node) BOOST_PP_TUPLE_ELEM(1, node) #define GEN_NAME(node) BOOST_PP_TUPLE_ELEM(2, node) #define GEN_CHILDS(node) \ // конвертируем в список, выкусываем хвост начиная с элемента 3 BOOST_PP_LIST_REST_N(3, BOOST_PP_TUPLE_TO_LIST(node)) // функтор для генерации полей класа вида "тип имя;" #define GENERATE_MEMBER_PRED(r, data, member) \ GEN_TYPE(member) GEN_NAME(member); // функтор для генерации вызовов worker внутри фнукции f #define GENERATE_WORK_CALL_PRED(r, data, member) \ BOOST_PP_IF(GEN_NODE_TYPE(member) \ // если тип "группа" , GEN_NAME(member).f(worker) \ // то "имя.f(worker)" , worker.work(BOOST_PP_STRINGAZE(GEN_NAME(member)) \ // заковычиваем имя ,GEN_NAME(2, member)) \ // иначе worker.work("имя", имя) ); // точка с запятой для всех #define GENERATE_CLASS(node) \ class GEN_TYPE(node) \ // "class тип" { \ public: \ \ // генерируем поля BOOST_PP_LIST_FOR_EACH( \ GENERATE_MEMBER_PRED \ // выполняем для каждого элемента , _ \ // здесь можно пусто , GEN_CHILDS(node) \ // список детей ) \ \ void f(Worker & worker) \ { \ worker.begin(BOOST_PP_STRINGAZE(GEN_TYPE(member))); \ // заковычивам тип \ \ // генерируем вызовы worker для полей BOOST_PP_LIST_FOR_EACH( \ GENERATE_WORK_CALL_PRED \ // выполняем для каждого ребенка , _ \ // здесь можно пусто , GEN_CHILDS(node) \ // список детей ) \ \ worker.end(); \ } \ }; Результат для GENERATE_CLASS(MY_STRUCT): class AType { public: int AInt; BType B; char AChar; void f(Worker & worker) { worker.begin("AType"); worker.work("AInt", AInt); B.f(worker); worker.work("AChar", AChar); worker.end(); } }; Как видите, классы для вложенных групп не генерируются, но это уже хоть что-то. Чтобы сделать вложенные группы необходимо реализовать обход входного дерева, при котором для каждого узла, если он группа, вызывать GENERATE_CLASS. Так как препроцессор не поддерживает рекурсию макросов, обход надо делать нерекурсивный со стеком. На boost::preprocessor по идее это возможно, но сложно, предлагаю это для самостоятельной проработки. Внимание! Самое большое ограничение такого подхода в том, что бесконечную структуру обработать не получится. Так как количество итераций в любом циклическом операторе boost::preprocessor ограничено, равно как и в любом другом подобном инструменте основанном на препроцессоре Си. Сейчас, например, максимальное количество итераций для FOR_* составляет 256. Поэтому для больших циклов вам нужно править библиотеку boost::preprocessor либо делать свой клон с каким вам надо количеством итераций.

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

Отправить комментарий