Как я Boost.Any с Boost.MPL дружил

в 9:57, , рубрики: boost, c++, mpl, метапрограммирование, ненормальное программирование, Песочница, метки: , , ,

Недавно мне пришлось работать с кодом, в котором задача передачи параметров произвольных типов решена с использованием стандартных STL контейнеров, параметризованных типом boost::any.
Например:

    void foo (std::vector<boost::any>& args) {
        // do smth.
    }

Предыдущий разработчик был не очень аккуратен и внутри функции работа с содержимым boost::any основывалась на предположении об исходном типе данных, то есть если преобразование boost::any_cast не проходило, то параметр пропускался. В определенных случаях такой способ обработки приемлем и примеры этой методики работы можно посмотреть тут.
Однако, мне хотелось несколько обобщить исходные предположения о типе данных.

Представим себе ситуацию, что в std::vector лежат данные следующих типов:
short, int, float, double, и есть функция

    double sum (std::vector<boost::any>& args) {

    }

которая рассчитывает сумму значений переданных параметров, вызывая boost::any_cast<double> для каждого элемента контейнера.
Посмотрим, что происходит при преобразовании boost::any_cast

    template<typename ValueType>
    ValueType * any_cast(any * operand)
    {
            // 
            // some code skipped
            //
            operand->type() == typeid(ValueType)
            ? &static_cast<any::holder<ValueType> *>(operand->content)->held
            : 0;
    }

Если значения type_info исходного типа и типа, к которому производится преобразование не совпадают, то возвращается 0. Но, возвращаясь к функции sum, в обычной ситуации мы вполне себе в состоянии записать

    double value=1+2L+3.0F+4.0;

просуммировав тем самым int, long, float и double. Подобного же поведения мы хотим добиться от функции sum.

То есть, при обработке значения boost::any, мы хотим сказать: «Эй, функция ‘Smth cast (any)’, если можешь, преобразуй any к типу Smth». А для того чтобы функция cast знала, какие типы она может привести к Smth нам потребуются списки типов. По сути, мы хотим задать список типов, которые можно безболезненно привести к требуемому типу.
Для описания списка преобразуемых типов не будем изобретать велосипед, а возьмем библиотеку boost::mpl и обнаружим там нужную нам концепцию последовательностей типов.

Теперь нам потребуется класс преобразователь типов в качестве замены boost::any_cast, а также класс диспетчер преобразователей типов, который

  1. Принимал бы тип, в который мы хотим выполнять преобразование ToType
  2. Принимал бы список типов CompatibleTypes из которых возможно преобразование в требуемый тип ToType
  3. На основании объекта boost::any возвращал бы нам подходящий преобразователь типа.

Класс преобразователь должен предоставлять нам функцию преобразования в требуемый тип, а также хранить указатель на объект type_info типа, из которого он может выполнить преобразование:

 template<class ToType>
 class Converter {
 public:
  virtual ~Converter {}

  virtual ToType cast(const boost::any& arg) = 0;

  explicit Converter(const type_info* info) : m_info(info) { }

  const type_info& type() {
   return *m_info;
  }

 private:

  const type_info* m_info;

 };

Почему функция cast объявлена чисто виртуальной станет понятно чуть ниже. Теперь опишем класс диспетчер:

    template<class CompatibleTypes, class ToType>
    class TypesDispatcher {

        std::vector<Converter<ToType>* > converters;

        struct wrapper {

        //code skipped
        };

        
    public:

        TypesDispatcher();
        
        Converter<ToType>* getConverter(const boost::any& arg);
    };

Класс диспетчер параметризуется последоваетельностью допустимых типов CompatibleTypes и типом к которому выполняется преобразование ToType. В классе хранится контейнер указателей на Converter. Нам нужно, чтобы каждый Converter был привязан к одному из типов, перечисленных в CompatibleTypes. Для этого унаследуемся от Converter и реализуем функцию cast:

    template <class FromType, class ToType>
    class TypeConverter : public Converter<ToType> {
        
    public:

        TypeConverter() : Converter(&typeid(FromType)) { }

        virtual ToType cast(const boost::any& arg) {
            return static_cast<ToType>(*boost::any_cast<FromType>(&arg));
        }
    };

Теперь у нас есть все, чтобы создать объекты преобразователи для всех типов в CompatibleTypes, а также реализовать функцию getConverter.

    // функтор обертка над контейнером преобразователей
    struct wrapper {
        explicit wrapper(std::vector<Converter<ToType>* >& converters) 
                    : m_converters(converters)
        {
        }

        template<class FromType>
        void operator()(FromType) {
            m_converters.push_back(new TypeConverter<FromType, ToType>());
        }
                
        std::vector<Converter<ToType>* >& m_converters;
    };
    //конструктор класса диспетчера типов
    TypesDispatcher() {
        boost::mpl::for_each<CompatibleTypes>(wrapper(converters));
    }

Для прохода по списку типов CompatibleTypes мы воспользовались boost::mpl::for_each, который принимает объект функцию, применяемую к каждому типу в списке на этапе выполнения, для этого инстанируя объект каждого типа.
Реализация функции getConverter проста и очевидна:

    Converter<ToType>* getConverter(const boost::any& arg) {
        std::vector<Converter<ToType>* >::const_iterator it = converters.begin();
        while(it != converters.end() ) {
            Converter<ToType>* converter = *it;
            if(converter->type() == arg.type())
                return converter;
            ++it;
        }
        return 0;
    }

Вот и всё. Теперь мы можем написать нечто вроде

    template<class CompatibleTypes, class ReturnType>
    ReturnType sum(std::vector<boost::any>& args) {
        TypesDispatcher<CompatibleTypes, ReturnType> dispatcher;
        std::vector<boost::any>::size_type i;
        ReturnType result = ReturnType();
        for(i=0; i < args.size(); i++) {
            Converter<ReturnType>* converter = dispatcher.getConverter(args[i]);
            if(converter)
                result += converter->to(args[i]);
        }
        return result;
    }
    int main(int argc, char* argv[]) {
        typedef boost::mpl::vector<int,long, float, double> types;

        std::vector<boost::any> seq;

        seq.push_back(boost::any( 1 ));
        seq.push_back(boost::any( 2L ));
        seq.push_back(boost::any( 3.0F ));
        seq.push_back(boost::any( 4.0 ));

        double result = sum<types, double>(seq); 
    }

Законченный пример кода можно посмотреть тут.

В итоге мы получили возможность работать с аргументами произвольных типов, относительно гибко настраивая правила их преобразования.

Автор: VnS

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js