Подтвердить что ты не робот

Переключатель времени работы Boost.Fusion

Я читаю тип объекта из файла:

enum class type_index { ... };
type_index typeidx = read(file_handle, type_index{});

В зависимости от индекса типа я хочу создать тип (из списка возможных типов) и сделать с ним что-то общее (тот же общий код для каждого типа):

std::tuple<type1, type2, ..., typeN> possible_types;

boost::fusion::for_each(possible_types, [&](auto i) {
  if (i::typeidx != typeidx) { return; }
  // do generic stuff with i
});

То есть:

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

Это выглядит как оператор switch с условием выполнения, но где "случаи" генерируются во время компиляции. В частности, это вообще не похоже на инструкцию for_each (я ничего не делаю для всех элементов в векторе, кортеже, списке, но только для одного элемента).

Есть ли более четкий способ выразить/написать эту идиому? (Например, используйте mpl::vector вместо std::tuple для возможных типов, используйте нечто, отличное от алгоритма for_each,...)

4b9b3361

Ответ 1

Мне нравится мой обычный унаследованный трюк лямбда:

Я писал об этом раньше

Я считаю, что я видел, как Сумант Тамбе использовал его в своих недавних сообщениях cpptruths.com.


Демонстрация

Вот демо на данный момент. Дальше добавим некоторые пояснения.

Самый важный трюк - использование boost::variant для скрытия демона кода кода для нас. Но этот принцип применяется, даже если вы придерживаетесь логики распознавания своего типа (просто требуя большего кодирования).

Live On Coliru

#include <boost/serialization/variant.hpp>
#include <boost/serialization/vector.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>

#include <fstream>
#include <iostream>

using namespace boost; // brevity

//////////////////
// This is the utility part that I had created in earlier answers:
namespace util {
    template<typename T, class...Fs> struct visitor_t;

    template<typename T, class F1, class...Fs>
    struct visitor_t<T, F1, Fs...> : F1, visitor_t<T, Fs...>::type {
        typedef visitor_t type;
        visitor_t(F1 head, Fs...tail) : F1(head), visitor_t<T, Fs...>::type(tail...) {}

        using F1::operator();
        using visitor_t<T, Fs...>::type::operator();
    };

    template<typename T, class F> struct visitor_t<T, F> : F, boost::static_visitor<T> {
        typedef visitor_t type;
        visitor_t(F f) : F(f) {}
        using F::operator();
    };

    template<typename T=void, class...Fs>
    typename visitor_t<T, Fs...>::type make_visitor(Fs...x) { return {x...}; }
}

using util::make_visitor;

namespace my_types {
    //////////////////
    // fake types for demo only
    struct A1 {
        std::string data;
    };

    struct A2 {
        double data;
    };

    struct A3 {
        std::vector<int> data;
    };

    // some operations defined on A1,A2...
    template <typename A> static inline void serialize(A& ar, A1& a, unsigned) { ar & a.data; } // using boost serialization for brevity
    template <typename A> static inline void serialize(A& ar, A2& a, unsigned) { ar & a.data; } // using boost serialization for brevity
    template <typename A> static inline void serialize(A& ar, A3& a, unsigned) { ar & a.data; } // using boost serialization for brevity

    static inline void display(std::ostream& os, A3 const& a3) { os << "display A3: " << a3.data.size() << " elements\n"; }
    template <typename T> static inline void display(std::ostream& os, T const& an) { os << "display A1 or A2: " << an.data << "\n"; }

    //////////////////
    // our variant logic
    using AnyA = variant<A1,A2,A3>;

    //////////////////
    // test data setup
    AnyA generate() { // generate a random A1,A2...
        switch (rand()%3) {
            case 0: return A1{ "data is a string here" };
            case 1: return A2{ 42 };
            case 2: return A3{ { 1,2,3,4,5,6,7,8,9,10 } };
            default: throw std::invalid_argument("rand");
        }
    }

}

using my_types::AnyA;

void write_archive(std::string const& fname) // write a test archive of 10 random AnyA
{
    std::vector<AnyA> As;
    std::generate_n(back_inserter(As), 10, my_types::generate);

    std::ofstream ofs(fname, std::ios::binary);
    archive::text_oarchive oa(ofs);

    oa << As;
}

//////////////////
// logic under test
template <typename F>
void process_archive(std::string const& fname, F process) // reads a archive of AnyA and calls the processing function on it
{
    std::ifstream ifs(fname, std::ios::binary);
    archive::text_iarchive ia(ifs);

    std::vector<AnyA> As;
    ia >> As;

    for(auto& a : As)
        apply_visitor(process, a);
}

int main() {
    srand(time(0));

    write_archive("archive.txt");

    // the following is c++11/c++1y lambda shorthand for entirely compiletime
    // generated code for the specific type(s) received
    auto visitor = make_visitor(
        [](my_types::A2& a3) { 
                std::cout << "Skipping A2 items, just because we can\n";
                display(std::cout, a3);
            },
        [](auto& other) { 
                std::cout << "Processing (other)\n";
                display(std::cout, other);
            }
        );

    process_archive("archive.txt", visitor);
}

Печать

Processing (other)
display A3: 10 elements
Skipping A2 items, just because we can
display A1 or A2: 42
Processing (other)
display A1 or A2: data is a string here
Processing (other)
display A3: 10 elements
Processing (other)
display A1 or A2: data is a string here
Processing (other)
display A1 or A2: data is a string here
Processing (other)
display A3: 10 elements
Processing (other)
display A1 or A2: data is a string here
Processing (other)
display A3: 10 elements
Processing (other)
display A3: 10 elements

Ответ 2

Я думаю, что ваше существующее решение неплохое. В точке // do generic stuff вместо этого вызовите другие функции, перегруженные по типу

boost::fusion::for_each(possible_types, [&](auto i) {
  if (i::typeidx != typeidx) { return; }
  doSpecificStuff(i);
});

void doSpecificStuff(const TypeA& a) { ... }
void doSpecificStuff(const TypeB& b) { ... }
...

AFAIK вы не можете получить switch, который немного быстрее, чем структура if...else здесь, но не существенно и вряд ли будет заметен для процесса, который вы запускаете при чтении файла.

Другие варианты похожи. Контейнеры с произвольным доступом Fusion или mpl или даже std:: tuple могут иметь доступ с помощью get < > , но для этого требуется индекс времени компиляции, поэтому вы создаете случаи и все еще просматриваете индексы с чем-то вроде

if (idx == 0) { doSpecificStuff(std::get<0>(possible_types)); }
else if (idx == 1) ...
....

Что можно сделать с помощью рекурсивных шаблонов, например:

template <size_t current>
void dispatchImpl(size_t idx)
{
    if (idx >= std::tuple_size<possible_types>::value) return;
    if (idx == current) 
    {
        doSpecificStuff(std::get<current>(possible_types));
        return;
    }
    dispatchImpl<current + 1>(idx);
}
void dispatch(size_t idx) { dispatchImpl<0>(idx); }

Единственной альтернативой, о которой я знаю, было бы создание массива указателей на функции. См. Оптимальный способ доступа к элементу std:: tuple во время выполнения по индексу. Я не думаю, что вы действительно получите что-то с этим решением для своего дела, и вам будет труднее следовать.

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

Ответ 3

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

Если это так, вы можете сопоставить каждый type_index с std::function, чтобы заставить компилятор генерировать код для каждого типа и иметь простой способ вызова каждой функции в замене коммутатора.

Замена коммутатора:

function_map.at(read())();

Пример выполнения:

#include <stdexcept>
#include <map>
#include <string>
#include <functional>
#include <iostream>

template<typename Type>
void doGenericStuff() {
    std::cout << typeid(Type).name() << std::endl;
    // ...
}

class A {};
class B {};
enum class type_index {typeA, typeB};
const std::map<type_index, std::function<void()>> function_map {
    {type_index::typeA, doGenericStuff<A>},
    {type_index::typeB, doGenericStuff<B>},
};

type_index read(void) {
    int i;
    std::cin >> i;
    return type_index(i);
}

int main(void) {
    function_map.at(read())(); // you must handle a possible std::out_of_range exception
    return 0;
}

Ответ 4

Я бы сказал, что лучше всего просто использовать массив функций, которые делают то, что вы хотите сделать:

typedef std::tuple<type1, type2, ..., typeN> PossibleTypes;
typedef std::function<void()> Callback;

PossibleTypes possible_types;
std::array<Callback, std::tuple_size<PossibleTypes >::value> callbacks = {
    [&]{ doSomethingWith(std::get<0>(possible_types)); },
    [&]{ doSomethingElseWith(std::get<1>(possible_types)); },
    ...
};

Этот массив легко сгенерирован с помощью integer_sequence, если все ваши вызовы действительно одинаковы:

template <typename... T, size_t... Is>
std::array<Callback, sizeof...(T)> makeCallbacksImpl(std::tuple<T...>& t,
                                                     integer_sequence<Is...>)
{
    return { [&]{ doSomethingWith(std::get<Is>(t)) }... };

    // or maybe if you want doSomethingWith<4>(std::get<4>(t)):
    // return { [&]{ doSomethingWith<Is>(std::get<Is>(t)) }... };

}

template <typename... T>
std::array<Callback, sizeof...(T)> makeCallbacks(std::tuple<T...>& t) {
    return makeCallbacksImpl(t, make_integer_sequence<sizeof...(T)>{});
}

И как только у нас будет наш массив, независимо от того, каким образом мы сгенерируем его, нам просто нужно его назвать:

void genericStuffWithIdx(int idx) {
    if (idx >= 0 && idx < callbacks.size()) {
        callbacks[idx]();
    }
    else {
        // some error handler
    }
}

Или если бросок достаточно хорош:

void genericStuffWithIdx(int idx) {
    callbacks.at(idx)(); // could throw std::out_of_range
}

Вы не можете по-настоящему обыгрывать массив по производительности, хотя у вас есть косвенное отношение через std::function<void()>. Это, безусловно, будет бить решение fusion for_each, поскольку даже если idx == 0, вы все равно выполняете каждый элемент. В этом случае вы действительно хотели бы использовать any(), поэтому вы можете выйти раньше. Но еще быстрее использовать массив.

Ответ 5

Создайте unordered_map от type_index до кода обработки.

Прочитайте type_index, найдите в карте, выполните. Ошибка проверки отсутствующих записей.

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