Комментарии 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++ не ощущается как язык, на котором подобные вещи делать легко. Но, с другой стороны, с каждым стандартом он становится все более и более удобным. И это ты сам в своей работе ощущаещь.
— Del --
А вот так будет выглядеть указание маршрутов и вызов обработчиков:
Я правильно понимаю, что коллбеки, передаваемые в 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
нет конструктора по умолчанию?
А это фундаментальное ограничение или от него можно избавиться? Или можно, но ценой большой переделки кода?
Вообще-то наиболее правильным ответом будет: так получилось. Т.е. была сделана первая, наиболее простая версия, которая закрывала насущные потребности. В этих насущных потребностях не было нужды работать с типами, которые не являются DefaultConstructible.
Если такая надобность возникнет (например, если кто-то откроет issue с описанием реальной ситуации), то тогда и будем думать, как это преодолеть.
Может быть, все решится дополнительной перегрузкой для produce
, может быть нужно будет что-то посложнее. Посмотрим, когда такая потребность возникнет.
Продолжаем упарываться многоэтажными С++ными шаблонами в RESTinio: безопасная по типам альтернатива express-js роутеру