Lua*, C++*

Разработка → Генератор умных перечислений, EnumGenerator

JDTamerlan 13 января 2015 в 09:55 4,5k
Привет всем!

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

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

  • Числовой индекс — это уникальное целочисленное значение, элемент последовательного массива, для С/С++ это диапазон [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
Проголосовать:
+4
Сохранить: