Простой инлайн-визитор для boost::variant

    Привет, Хабр.

    Одним прекрасным пятничным вечером я писал обработку ошибок в одном своем хобби-проекте… Так, это вступление для другой статьи.
    В общем, одним прекрасным пятничным вечером мне потребовалось пройтись по 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& vec) {} написать нельзя. Повод отправить пропозал к C++17.
    • +16
    • 10,6k
    • 8
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 8
    • +3
      Насчёт более тонкого паттерн-матчинга:

      template< typename T >
      struct is_cref_vector
          : std::false_type
      { ; }
      
      template< typename T >
      struct is_cref_vector< std::vector< T > const & >
          : std::true_type
      { ; }
      
      [] (auto const & v) -> std::enable_if_t< is_cref_vector< decltype(v) >{} > { /* ... */ }
      
      • +1
        Спасибо, отличное дополнение!

        К сожалению, объявлять такие шаблонные предикаты, что ли, внутри функции не получится — темплейты не могут быть в block scope. Но это, конечно, лучше, чем ничего.

        Впрочем, если определять извне, то там в качестве альтернативного варианта можно и полноценную структуру с перегруженным operator() объявить, вроде
        struct VectorHandler
        {
            template<typename T>
            void operator() (const std::vector<T>& vec)
            {
                // ...
            }
        };
        
        void Foo ()
        {
            Visit (v,
                    [] (const ConcreteType&) { ... },
                    VectorHandler {});
        }
        


        Кстати, что интересно, локальная структура не может иметь шаблонных функций-членов, а ведь generic lambda именно в такую штуку и развернется. Непоследовательно как-то.
        • 0
          Последнее замечание — по-моему верное. Судя по всему, такой запрет существует до сих пор лишь только по историческим причинам.
          • 0
            Можно сделать «метафабрику» предикатов для кастомизации обобщённых лямбда-функций.
        • +1
          В классе
          VisitorBase
          нет using-директив для «выноса» всех операторов
          operator ()
          в most-derived класс. Здесь есть указание на то, что если компилятор принимает такой код, то это не соответствует стандарту. Здесь подробно обсуждается код, эквивалентный вашему. Этот вопрос тоже относится к проблеме.
          • +1
            Спасибо! Третья ссылка самая важная, с отсылкой к конкретному месту в стандарте.

            Печально, что using Args::operator()... или вроде того не написать.
            • 0
              Используйте рекурсию при наследовании, как это делают все.
              P.S.: А по-моему самое важное всегда — это рабочий пример.
              • +1
                Безусловно, вечером допишу update в статью.

                Надо было проверять не только clang'ом.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.