Генератор умных перечислений, EnumGenerator

    Привет всем!

    Несколько лет назад меня начал беспокоить вопрос создания статических (создаваемых и изменяемых до процесса компиляции) перечислений. Перечислений я хотел не простых, которые реализованы в С/С++, а с набором дополнительных возможностей, в том числе и ассоциированными столбцами данных произвольного типа, своего рода статическая база данных с доступом по уникальному идентификатору.

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

    • Числовой индекс — это уникальное целочисленное значение, элемент последовательного массива, для С/С++ это диапазон [0;n), где n — размер массива. Если мы видим индекс 5, то это подразумевает, что обязательно есть и индексы [0;4]. Примеры: индекс классического С-массива, хеш-таблица, адрес ячейки физической памяти. Краткий вывод: скорость обработки максимальная, сопровождаемость минимальная.
    • Числовой идентификатор — это уникальное целочисленное значение, которое лишено обязанности быть последовательным. Примеры: дескриптор произвольного (файл, сокет, устройство) объекта, идентификатор потока, адрес какой-то переменной или функции в процессе. Краткий вывод: скорость обработки высокая, сопровождаемость средняя.
    • Символьный идентификатор — это уникальное строковое значение, которое, в отличие от чисел, само по себе наделено некоторым логическим смыслом. Примеры: препроцессорное определение с помощью #define, классическое перечисление с помощью enum, название переменной в программе, ключ в объекте json, значение в формате XML. Краткий вывод: скорость обработки минимальная, сопровождаемость максимальная.

    Основная задача проекта EnumGenerator — сгенерировать перечисление, которое удобно, безопасно и эффективно объединяет эти идентификаторы в единую конструкцию. Чтобы к одному значению можно было обратиться тремя способами:
    1. Очень быстро по числовому индексу. Для основного использования в тексте программы. Пример: обычное перечисление.
    2. Быстро и гибко по числовому идентификатору. Для сохранения значения в энергонезависимое хранилища и безопасное восстановление из него. Для обмена данными с другими программами, которые могут иметь более старую или более новую версию этого же перечисления. Пример: база данных, протокол сетевого взаимодействия.
    3. Удобно и наглядно по символьному идентификатору. Для сохранения значения в конфигурационный файлы, который может редактироваться человеком, и безопасное восстановление из него. Пример: файл конфигурации *.ini, *.json, *.yaml,*.xml.

    Входные данные EnumGenerator, перечисление Color3 в таблице Excel:


    Выходные данные EnumGenerator:
    Перечисление Color3 в файле 'Enums.hpp'
    class Color3
    {
    public:
        enum Value
        {
            Black = 0, //! men in black
            Blue = 1, //! blue ocean
            Green = 2, //! green forest
            Invalid = 3,
            Red = 4, //! lady in red
            White = 5 //! white snow
        };
        static const int ValueCount = 6;
        static const Value ValueInvalid = Invalid;
    
        Color3(Value val = ValueInvalid) : m_ValueCur(val) {}
        Color3(const Color3 &other) : m_ValueCur(other.m_ValueCur) {}
        explicit Color3(int val) : m_ValueCur(ValueInvalid)
        {
            int index = NS_JDSoft::NS_EnumGenerator::ValueFindLinear(IDInteger, ValueCount, val);
            if (index >= 0) m_ValueCur = Value(index);
        }
        explicit Color3(const char * val) : m_ValueCur(ValueInvalid)
        {
            int index = NS_JDSoft::NS_EnumGenerator::StringFindBinary(StringValues, ValueCount, val);
            if (index >= 0) m_ValueCur = Value(index);
        }
    
        Color3 &operator =(Value val) { m_ValueCur = val; return *this; }
        Color3 &operator =(const Color3 &other) { m_ValueCur = other.m_ValueCur; return *this; }
    
        bool operator ==(Value val) const { return m_ValueCur == val; }
        bool operator ==(const Color3 &other) const { return m_ValueCur == other.m_ValueCur; }
    
        bool isValid() const { return m_ValueCur != ValueInvalid; }
    
        Value toValue() const { return m_ValueCur; }
        int toInt() const { return IDInteger[m_ValueCur]; }
        const char * toString() const { return StringValues[m_ValueCur]; }
    
        static const char * enumName() { return "Color3"; }
    private:
        static const char * const StringValues[ValueCount];
        static const int IDInteger[ValueCount];
    
        Value m_ValueCur;
    };
    


    Перечисление Color3 в файле 'Enums.cpp'
    const char * const Color3::StringValues[Color3::ValueCount]=
    {
        "Black",
        "Blue",
        "Green",
        "Invalid",
        "Red",
        "White"
    };
    
    const int Color3::IDInteger[Color3::ValueCount] =
    {
        0,
        255,
        65280,
        -1,
        16711680,
        16777215
    };
    


    Пример использования перечисления Color3 в тестовом проекте Qt:
    #include "Enums.hpp"
    ...
    // Создание объекта перечисления и проверка невалидности его значения
    Color3 colorPen;
    QCOMPARE(colorPen.isValid(), false); // isValid()
    QVERIFY(colorPen == Color3::Invalid); // operator ==(Value val)
    
    // Проверка правильности основных преобразований
    QCOMPARE(colorPen.toValue(), Color3::Invalid); // toValue()
    QCOMPARE(colorPen.toInt(), -1); // toInt()
    QCOMPARE(colorPen.toString(), "Invalid"); // toString()
    
    // Задание валидного значения
    colorPen = Color3::Red; // operator =(Value val)
    QCOMPARE(colorPen.isValid(), true);
    
    // Проверка правильности основных преобразований
    QCOMPARE(colorPen.toValue(), Color3::Red);
    QCOMPARE(colorPen.toInt(), 0xFF0000);
    QCOMPARE(colorPen.toString(), "Red");
    
    // Создание объекта с известным значением
    QCOMPARE(Color3(Color3::Green).toString(), "Green");
    QCOMPARE(Color3(0x00FF00).toString(), "Green");
    QCOMPARE(Color3("Green").toString(), "Green");
    
    // Сравнение объектов
    QVERIFY(Color3(0x0000FF) == Color3("Blue")); // operator ==(const Color3 &other)
    

    Как попробовать EnumGenerator?
    1. Скачать проект на свой компьютер, Download zip и распаковать его.
    2. Запустить файл Enums.lua в интерпретаторе lua.
      Самый простой способ для пользователей windows:
      1) Скачать минимальную версию интерпретатора lua и распаковать его.
      2) Щелкнуть правой кнопкой мыши по файлу 'Enums.lua', кликнуть «Открыть» или «Открыть с помощью» и выбрать распакованный файл 'lua.exe'.
    3. Убедиться, что в директории сгенерировались файлы 'Enums.hpp' и 'Enums.cpp'. Готово! :)

    Как использовать EnumGenerator в своем проекте?
    1. Попробовать EnumGenerator как описано в предыдущем пункте.
    2. Переместить директорию с файлом 'EnumGenerator.lua' в стабильное место.
      Путь к директории с файлом 'EnumGenerator.lua' будем называть 'ENUMGENERATOR_PATH'.
    3. Открыть файл 'Enums.lua' и отредактировать его так, чтобы переменная EnumGenerator содержала полный путь к генератору.
      Для этого и последующих проектов это нужно сделать один раз.
    4. Скопировать файлы 'Enums.xls' и 'Enums.lua' в директорию своего проекта.
    5. Отредактировать файл 'Enums.xls' в Excel или OpenOffice для своих перечислений.
    6. Сохранить отредактированный файл 'Enums.xls' как есть, дополнительно сохранить его в формате csv в файл 'Enums.csv' и закрыть редактор.
    7. Запустить файл Enums.lua из директории своего проекта в интерпретаторе lua и убедиться, что генерация прошла успешно:
      • При запуске в консольном режиме скрипт пишет «Successfully completed!».
      • При запуске из другой программы код возврата интерпретатора равен 0.

    Схема прохождения данных при использовании EnumGenerator:


    Вот пример, который наглядно демонстрирует основные возможности использования генератора, файл Excel.

    Рассмотрим часть этого файла более подробно:


    Структура входного файла:
    1. Файл состоит из независимых частей-блоков, [0;∞]. Каждая часть (Part) начинается словом «PartBegin» и заканчивается словом «PartEnd» в самой левой ячейке. После «PartBegin» в этой же ячейке в круглых скобках указываются обязательные и опциональные параметры этой части, пример: PartBegin(Type=Enum, Name=Country). Все строки между «PartBegin» и «PartEnd» являются телом части (PartData). Последовательность частей учитывается.
    2. Тело части (PartData) состоит из 0-го столбца (самый левый), который определяет тип всей строки и последующих ячеек [0;∞]. Сейчас реализовано 3 типа: Header — заглавие данных, Comment — комментарий, ""(пустая строка) — содержит основные данные в соответствии с заглавием.
    3. Заглавие данных (Header) описывает обязательный идентификатор столбца и возможные дополнительные параметры.

    Надеюсь, это выглядит наглядным, простым и логичным.

    Буду рад отзывам и советам по улучшению, спасибо!

    Связанные ссылки:
    String enum — строковые enum
    Ещё одна реализация Enums для Python
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 9
    • 0
      Думаю использовать xls в качестве входного файла не лучший вариант.
      Plain text с каким-то простеньким синтаксисом будет в самый раз.
      • 0
        Ну так скрипт, насколько я понял, генерит из CSV — куда уже проще. А XLS только для удобного наглядного создания. Хотя я бы тоже отдал предпочтение исходнику в чем-то, вроде комментированного JSON
        • 0
          Например, так?
          {
              black = 0,  
              darkred = {1, "Bloody Mary"},
              white = 15  -- Snow white won't get commented
          }
          • 0
            Да, вполне ничего
            • 0
              Для простых вещей выглядит просто и удобно.
              Хотя если будет больше двух значений (столбцов) в объекте, то уже нужны будут ключи.
              Если задать тип дополнительного столбца и значение по умолчанию, то наглядность потеряется.

              В моем варианте тоже есть неудобство, что в систему контроля версий нужно добавлять не только xls, но и csv, чтобы можно было наглядно diff посмотреть.
              • –1
                Скорее так:
                enum Color
                {
                    black = 0,  
                    darkred = {1, "Bloody Mary"},
                    white = 15  -- Snow white won't get commented
                }
                


                Мне кажется языку не хватает встроенной возможности расширения синтаксиса.
                А то немного «улучшенную» версию enum'ов пришлось ждать до C++11. И все вкусняшки в языке нужно протаскивать через комитет по стандартизации.

                Если бы в языке была бы стандартная возможность расширять синтаксис — хотя бы просто на уровне синтаксического сахара, как в данном примере. То многие улучшения могли быть обкатаны заранее и самые востребованные и «выжившие » в конкурентной борьбе пошли бы в стандарт. По аналогии с расширением стандартной библиотеки за счет переноса каких-то вещей из буста.

                Плюс нишевые расширения, а ля Qt moc generator, были бы реализованы без дополнительных зависимостей и пре-генерации (вернее она стала бы стандартным шагом при обработке исходных кодов, наравне с раскрытием макросов, компиляцией и т.п.).

                Хотя Страуструп и опасался того, что разные компиляторы C++ вводят дополнительные несовместимые языковые конструкции. Я думаю в этом вопросе нужно было не противостоять этой тенденции, а возглавить её. То есть стандартизировать её и вопрос совместимости сам бы отпал.
                • 0
                  Похоже лед тронулся:
                  metaclasses
                  • 0

                    Спасибо за информацию!


                    Мне показалось, что снова идет усложнение синтаксиса. Зачем???
                    Подход Qt намного проще — они используют обычные возможности С++, но вводят еще один уровень выполнения-генерации когда (Qt-moc). Их решение мне кажется вполне эффективным.


                    Т.е. надо не придумывать новые конструкции языка в большом количестве, а ввести новое время генерации-выполнения: MacroTime, GenerationTime, CompileTime, RunTime,

                    • 0
                      По сути так и есть. Мета-классы применяются перед компиляцией.
                      Никакого усложнения синтаксиса нет — на вход метакалссы принимают синтаксически валидный С++ класс. Сама реализация пишется вполне обычными С++ циклами, условными операторами и т.п. Единственное дополнение — знак $, который они заменят на что-то другое.

                      Но в вашем подходе нужно будет тоже как-то различать код для этапа компиляции и генерации, тоже нужно вводить какое-то волшебное слово.

                      Подход Qt — это очень серьезный компромисс, по сути других вариантов не было в тот момент.
                      Они используют синтаксис макросов, что достаточно ограниченно в выразительных средствах и нам все равно приходится запоминать синтаксис разных Q_PROPERTY или Q_ENUM.

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