Как стать автором
Обновить

Комментарии 9

Крутая статья! Обычно, парсинг — это дело крайне неблагородное и неприятное. Немногие желают этим заниматься. И все дело в том, что часто находятся редкие баги то тут, то там.


Когда есть инструмент, который позволяет отслеживать ошибки во время компиляции — то это всегда полезно и правильно.


Одно только смущает: приходится серьезно упарываться шаблонами, чтобы написать всего-лишь нормальный парсер.

Крутая статья!

Спасибо!


Обычно, парсинг — это дело крайне неблагородное и неприятное. Немногие желают этим заниматься.

А выбора-то особо и не было. Либо затаскивать в проект какую-то тяжелую зависимость (типа Boost.Spirit или PEGTL), либо делать самому. Тем более, что у Boost.Spirit и PEGTL есть особенность, которая была show-stopper-ом: они информируют об ошибках парсинга исключениями. А исключения больно бьют по производительности, тем более что в предполагаемых сценариях ошибок парсинга можно ожидать много.


Одно только смущает: приходится серьезно упарываться шаблонами, чтобы написать всего-лишь нормальный парсер.

Это да. С другой стороны становится наглядно заметно, как развивается и улучшается в этом плане C++:


  • C++11 сделал возможным работу с variadic templates, плюс появились using-и и лямбды;
  • в С++14 появилось возможность в качестве результирующего значения функции указывать auto, без необходимости дальше уточнять тип результата. Плюс полиморфные лямбды;
  • в C++17 появились fold expressions. Как раз в RESTinio из-за желание сохранить совместимость с C++14 довелось искать способ обойтись без fold expressions;
  • в C++20 появились концепты и куча enable_if из реализации easy_parser может быть просто выброшена в C++20. А какая-то часть compile-time проверок там же может быть более компактно и понятно выражена через синтаксис концептов.

Так что, с одной стороны, C++ не ощущается как язык, на котором подобные вещи делать легко. Но, с другой стороны, с каждым стандартом он становится все более и более удобным. И это ты сам в своей работе ощущаещь.

А вот так будет выглядеть указание маршрутов и вызов обработчиков:

Я правильно понимаю, что коллбеки, передаваемые в router->http_get, считаются независимыми функциями? Просто в продемонстрированном примере, кмк, проще было бы переделать router таким образом, чтобы он хранил в себе объект-обработчик API, а в http_get передавать указатели на методы api_v1_handler.


Отличие path_to_tuple от path_to_params состоит в том, что path_to_tuple передает все сгенерированные producer-ами значения в обработчик в виде одного единственного тупла. Например:

Можно ли в этом примере написать лямбду как [handler](const auto & req, auto params) { ... }? Или вывод типов не справляется?


И, соответственно, специализация для std::array:

С учётом того, что в трансформерах могут быть произвольные функции, в том числе и бросающие исключения, возникает вопрос, что происходит, если исключение будет выброшено во время конструирования такого частичного std::array. Будут ли корректно вызваны деструкторы у его элементов и не будут ли они вызваны на не инициализированных элементах?

Я правильно понимаю, что коллбеки, передаваемые в router->http_get, считаются независимыми функциями?

Да.


Просто в продемонстрированном примере, кмк, проще было бы переделать router таким образом, чтобы он хранил в себе объект-обработчик API, а в http_get передавать указатели на методы api_v1_handler.

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


Если кому-то нужно, чтобы router делегировал все вызовы в какой-то один объект-обработчик, то это можно сделать самому. Или открыть issue показав, что это действительно полезная штука.


Можно ли в этом примере написать лямбду как [handler](const auto & req, auto params) {… }?

Можно. Явно тип тупла в статье был описан чтобы показать, какой тип придет в качестве параметра.


Или вывод типов не справляется?

Здесь вывод типов не при чем. Типы параметров вычисляются отдельно и независимо от формата функтора-обработчика.


что происходит, если исключение будет выброшено во время конструирования такого частичного std::array. Будут ли корректно вызваны деструкторы у его элементов и не будут ли они вызваны на не инициализированных элементах?

В std::array не может быть неинициализированных элементов в данном контексте. Все элементы проинициализированы конструктором по умолчанию для хранящегося в std::array типа. Соответственно, если при модификации i-го элемента для этого std::array вылетает исключение, то временный std::array уничтожается и вызываются деструкторы для всех его элементов, как уже модифицированных, так и тех, что были проинициализированы дефолтным конструктором.

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

То есть std::array<T> нельзя сконструировать, если у T нет конструктора по умолчанию?

Сам по себе std::array можно. Это ограничение RESTinio-всеого easy_parser-а: при вызове produce<T>() тип T должен быть DefaultConstructible. Даже если в качестве T будет std::array<Y, S>.

А это фундаментальное ограничение или от него можно избавиться? Или можно, но ценой большой переделки кода?

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


Если такая надобность возникнет (например, если кто-то откроет issue с описанием реальной ситуации), то тогда и будем думать, как это преодолеть.


Может быть, все решится дополнительной перегрузкой для produce, может быть нужно будет что-то посложнее. Посмотрим, когда такая потребность возникнет.

Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории