Привет, Хабр.
Одним прекрасным пятничным вечером я писал обработку ошибок в одном своем хобби-проекте… Так, это вступление для другой статьи.
В общем, одним прекрасным пятничным вечером мне потребовалось пройтись по boost::variant
и что-то сделать с лежащими там данными. Вполне себе стандартная задача для boost::variant
, и каноничный (но очень многословный) способ её решения — описать наследующуюся от boost::static_visitor
структуру с перегруженными operator()
и передать её в boost::apply_visitor
. И вот этим прекрасным вечером мне почему-то стало очень лень писать всю эту кучу кода, и захотелось заиметь какой-то более простой и краткий способ описания визиторов. Что из этого вышло, можно почитать под катом.
Так вот, каноничный способ выглядит как-то так:
using Variant_t = boost::variant<int, char, std::string, QString, double, float>; template<typename ValType> struct EqualsToValTypeVisitor : boost::static_visitor<bool> { const ValType Value_; EqualsToValTypeVisitor (ValType val) : Value_ { val } { } bool operator() (const std::string& s) const { return Value_ == std::stoi (s); } bool operator() (const QString& s) const { return Value_ == s.toInt (); } template<typename T> bool operator() (T val) const { return Value_ == val; } }; void DoFoo (const Variant_t& var) { const int val = 42; if (boost::apply_visitor (EqualsToValTypeVisitor<int> { val }, var)) // ... }
И это мы ещё воспользовались тем, что четыре случая для int
, char
, float
и double
можно описать одним шаблонным оператором, иначе операторов было бы ещё на три больше, код был бы ещё более раздутым, и выглядело бы всё ещё более ужасно.
Кроме того, когда функции-обработчики конкретных типов коротенькие, как-то совсем жалко под них заводить отдельную структуру, вытаскивать их далеко от функции, в которой они используются, и так далее. А ещё приходится писать конструктор, если надо передать какие-то данные из точки применения визитора в сам визитор, приходится заводить поля под эти данные, приходится следить за копированием, ссылками и прочими вещами. Всё это начинает не очень приятно пахнуть.
Возникает естественный вопрос: а можно ли как-то определять визиторы прямо в месте использования, да ещё с минимумом синтаксического оверхеда? Ну, чтобы прямо
void DoFoo (const Variant_t& var) { const int val = 42; const bool isEqual = Visit (var, [&val] (const std::string& s) { return val == std::stoi (s); }, [&val] (const QString& s) { return val == s.toInt (); }, [&val] (auto other) { return other == val; }); }
Оказывается, можно.
Так как решение оказывается на удивление простым и по-своему изящным, и писать его совсем сразу неинтересно (статья больно короткая получится), я также немного опишу, как я к этому решению пришёл, так что следующие два-три абзаца можно пропустить.
Моей первой попыткой, в детали реализации которой я вдаваться не буду (но которую можно потыкать здесь), было запихивание всех лямбд в std::tuple
и последовательный их перебор в шаблонном operator()
собственного класса, их хранящего, пока не удастся вызвать какую-нибудь функцию с аргументом, переданным самому operator()
.
Очевидным недостатком такого решения является фатально некорректная обработка типов, приводимых друг к другу, и зависимость от порядка передачи лямбд в функцию создания визитора. Так, рассмотрим упомянутый выше Variant_t
, имеющий среди прочих int
и char
. Если он создан с типом char
, а в функцию создания визитора первой была передана лямбда, принимающая int
, то она же первым делом и вызовется (и успешно!), а до случая для char
дело не дойдёт. Причём эта проблема действительно фатальна: для тех же int
и char
невозможно (по крайней мере, без существенных извращений) так определить порядок лямбд, чтобы для и int
, и char
передавались туда, куда надо, без всяких преобразований типов.
Однако, теперь стоит вспомнить, что такое лямбда и во что она разворачивается компилятором. А разворачивается она в некоторую анонимную структуру с переопределённым operator()
. А если у нас есть структура, то от неё можно отнаследоваться, и её operator()
автоматически окажется в соответствующей области видимости. А если отнаследоваться от всех структур сразу, то все их operator()
‘ы попадут куда надо, и компилятор автоматически выберет нужный оператор для вызова с каждым конкретным типом, даже если типы друг в друга приводятся (как в упомянутом выше случае int
и char
).
А дальше — дело техники и variadic templates:
namespace detail { template<typename... Args> struct Visitor : Args... // да, тут тоже можно разворачивать variadic pack { Visitor (Args&&... args) : Args { std::forward<Args> (args) }... // и тут можно { } }; }
Попытаемся написать функцию, которая берёт boost::variant
и набор лямбд и посещает этот самый variant
:
template<typename Variant, typename... Args> auto Visit (const Variant& v, Args&&... args) { return boost::apply_visitor (detail::Visitor<Args...> { std::forward<Args> (args)... }, v); }
Оп, получили ошибку компиляции. apply_visitor
ожидает получить наследника boost::static_visitor
, по крайней мере, в моей версии Boost 1.57 (говорят, позже была добавлена поддержка автоматического вывода возвращаемого типа в C++14-режиме).
Как получить тип возвращаемого значения? Можно попробовать взять, например, первую лямбду из списка и вызвать её со сконструированным по умолчанию объектом, что-то вроде
template<typename Variant, typename Head, typename... TailArgs> auto Visit (const Variant& v, Head&& head, TailArgs&&... args) { using R_t = decltype (head ({})); //return boost::apply_visitor (detail::Visitor<Head, TailArgs...> { std::forward<Head> (head), std::forward<TailArgs> (args)... }, v); }
При этом мы, естественно, предполагаем, что все лямбды возвращают один и тот же тип (или, точнее, все возвращаемые типы конвертируемы друг в друга).
Проблема такого решения в том, что этот самый объект может не иметь конструктора по умолчанию. std::declval
нам тут тоже не поможет, потому что тип, принимаемый первой лямбдой, мы не знаем наперёд, а пытаться вызвать её со всеми типами подряд из списка типов variant
— слишком костыльно и многословно.
Вместо этого мы поступим наоборот. Мы возьмём первый тип из списка типов variant
и вызовем наш уже сконструированный Visitor
с ним. Это гарантированно должно сработать, потому что визитор обязан уметь обработать любой из типов в variant
. Итак:
template<typename HeadVar, typename... TailVars, typename... Args> auto Visit (const boost::variant<HeadVar, TailVars...>& v, Args&&... args) -> { using R_t = decltype (detail::Visitor<Args...> { std::forward<Args> (args)... } (std::declval<HeadVar> ())); //return boost::apply_visitor (detail::Visitor<Args...> { std::forward<Args> (args)... }, v); }
Однако, сам Visitor
должен наследоваться от boost::static_visitor<R_t>
, а R_t
на этот момент неизвестен. Ну это уж совсем просто решить, разбив Visitor
на два класса, один из которых занимается наследованием от лямбд и агрегированием их operator()
‘ов, а другой — реализует boost::static_visitor
.
Итого получим
namespace detail { template<typename... Args> struct VisitorBase : Args... { VisitorBase (Args&&... args) : Args { std::forward<Args> (args) }... { } }; template<typename R, typename... Args> struct Visitor : boost::static_visitor<R>, VisitorBase<Args...> { using VisitorBase<Args...>::VisitorBase; }; } template<typename HeadVar, typename... TailVars, typename... Args> auto Visit (const boost::variant<HeadVar, TailVars...>& v, Args&&... args) { using R_t = decltype (detail::VisitorBase<Args...> { std::forward<Args> (args)... } (std::declval<HeadVar> ())); return boost::apply_visitor (detail::Visitor<R_t, Args...> { std::forward<Args> (args)... }, v); }
Для совместимости с C++11 можно добавить trailing return type вида
template<typename HeadVar, typename... TailVars, typename... Args> auto Visit (const boost::variant<HeadVar, TailVars...>& v, Args&&... args) -> decltype (detail::VisitorBase<Args...> { std::forward<Args> (args)... } (std::declval<HeadVar> ()))
Приятным бонусом является возможность работы с noncopyable-лямбдами (захватывающими в C++14-стиле какой-нибудь unique_ptr
, например):
#define NC nc = std::unique_ptr<int> {} Variant_t v { 'a' }; const auto& asQString = Visit (v, [NC] (const std::string& s) { return QString::fromStdString (s); }, [NC] (const QString& s) { return s; }, [NC] (auto val) { return QString::fromNumber (val); });
Недостатком является невозможность более тонкого паттерн-матчинга в стиле
template<typename T> void operator() (const std::vector<T>& vec) { //... }
К сожалению, [] (const std::vector<auto>& vec) {}
написать нельзя. Повод отправить пропозал к C++17.
ссылка на оригинал статьи http://habrahabr.ru/post/270689/
Добавить комментарий