пятница, 6 декабря 2019 г.

[prog.c++] Пользуюсь плодами упарывания шаблонами (PEG парсер в RESTinio)

Прикольный все-таки PEG-парсер получился в рамках развития RESTinio. Ниже показан код, который нужен для разбора вот такого выражения:

language-range = (1*8ALPHA *('-' 1*8alphanum)) / '*'

Т.е. language-range может быть представлен либо просто звездочкой либо последовательностью фрагментов в которой первый фрагмент может состоять только из букв (в количестве от одной до восьми штук), а последующие фрагменты начинаются с дефиса и могут содержать от одной до восьми букв или цифр.

Так вот при работе над очередным релизом RESTinio для разбора такого выражения был написан следующий код:

RESTINIO_NODISCARD
inline auto
make_language_tag_producer()
{
   return produce<std::string>(
         repeat(1u8u, alpha_symbol_producer() >> to_container()),
         repeat(0u, N,
               symbol_producer('-') >> to_container(),
               repeat(1u8u, alphanum_symbol_producer() >> to_container())
         )
   );
}

RESTINIO_NODISCARD
inline auto
make_language_range_producer()
{
   return produce<std::string>(
         alternatives(
               symbol_producer('*') >> to_container(),
               make_language_tag_producer() >> as_result()
         )
   );
}

Функция make_language_tag_producer() формирует объект, который будет отвечать за разбор первой альтернативы для language-range, а именно:

language-range = 1*8ALPHA *('-' 1*8alphanum)

Поэтому в make_language_tag_producer() практически так и говорится:

  • результатом разбора будет объект std::string. Этот объект будет служить результирующим контейнером, в который будут складываться найденные символы;
  • ожидается последовательность от одной до восьми букв. Каждая найденная буква добавляется в результирующий контейнер;
  • затем ожидается ноль или неограниченное количество повторений дефиса за которым следует последовательность из одной до восьми букв или цифр. Все найденные символы (дефис, буквы, цифры) добавляются в результирующий контейнер.

Функция make_language_range_producer() формирует объект, который будет выбирать одну из двух альтернатив. Что, опять же, практически прямолинейно и записано в коде:

  • результатом разбора будет std::string, который и будет результирующим контейнером для разобранных значений;
  • если во входном потоке встретится звездочка, то она должна быть сохранена в результирующем контейнере;
  • если же во входном потоке встретится вторая часть правила (парсер для второй части создает make_language_tag_producer), то этот результат разбора этой части должен стать и результатом всего правила.

В принципе, можно было бы записать и вот так:

inline auto
make_language_range_producer()
{
   return produce<std::string>(
         alternatives(
               symbol_producer('*') >> to_container(),
               produce<std::string>(
                     repeat(1u8u, alpha_symbol_producer() >> to_container()),
                     repeat(0u, N,
                           symbol_producer('-') >> to_container(),
                           repeat(1u8u, alphanum_symbol_producer() >> to_container())
                     )
               ) >> as_result()
         )
   );
}

Но мне проще было написать две маленьких функции с простыми правилами, чем одну с многословным правилом внутри.

Показанное выше правило для language-range является частью другого правила:

Accept-Language = 1#( language-range [ weight ] )
language-range  = <language-range, see [RFC4647], Section 2.1>

И парсер для Accept-Language записывается в коде вот так:

static auto
make_parser()
{
   using namespace accept_language_details;

   return produce< accept_language_value_t >(
      non_empty_comma_separated_list_producer< item_container_t >(
         produce< item_t >(
            make_language_range_producer() >> &item_t::language_range,
            maybe( weight_producer() >> &item_t::weight )
         )
      ) >> &accept_language_value_t::languages
   );
}

В общем, получилось прикольно. Пока более-менее нравится. Посмотрим как пойдет дальше работа над реализацией средств для разбора HTTP-полей. Если все будет хорошо и дальше, то может быть появится неплохая тема для рассказа на C++Russia в следующем году ;)

PS. Кому интересна ретроспектива эпопеи вокруг инструментов для парсинга, то можно начать отсюда и далее в историю по ссылкам на предыдущие блог-посты.

вторник, 3 декабря 2019 г.

[prog.c++] Похоже, что втоптался в неприятную особенность static constexpr членов класса

Почему-то пребывал в уверенности, что если в C++ объявлять статические члены класса как static constexpr, то компилятор с линкером автоматически будут разруливать ситуации с множественными определениями таких статических членов в разных единицах трансляции. Но не так все просто оказалось.

Дабы не быть голословным создал просто демо-пример.

Тут есть заголовочный файл qvalue.hpp, внутри которого объявляется класс со static constexpr полем maximum.

Есть два файла (a.cpp и b.cpp), которые внутри себя загружают qvalue.hpp и используют qvalue_t::maximum. Эти файлы компилируются по отдельности и результаты их компиляции затем собираются в один статический файл.

Так вот, если компилировать более-менее современными версиями GCC и clang (скажем, начиная от GCC-5.5 и clang-6.0) с оптимизацией, т.е.:

$ g++ -std=c++14 a.cpp b.cpp main.cpp -O2

то никаких проблем нет, все успешно компилируется и линкуется.

Но вот если собирать без оптимизации, т.е.:

$ g++ -std=c++14 a.cpp b.cpp main.cpp -O0

то возникает ошибка линковки:

/tmp/ccHv44S3.o: In function `a(unsigned int&)':
a.cpp:(.text+0x1e): undefined reference to `qvalue_t::maximum'
/tmp/ccIQACTW.o: In function `b(unsigned int&)':
b.cpp:(.text+0x1e): undefined reference to `qvalue_t::maximum'
collect2: error: ld returned 1 exit status

При этом в VC++ проблем нет.

Нахожусь сейчас в состоянии разорванного напрочь шаблона. Ибо это получается, что static constexpr поля, которые должны инициализироваться прямо в определении класса, оказываются неприменимы в header-only библиотеках.