Boost.Fusion run-time switch

我与影子孤独终老i 提交于 2020-01-31 07:09:08

问题


I am reading the type of an object from a file:

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

Depending on the type index, I want to create a type (out of a list of possible types), and do something generic with it (the same generic code for each type):

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
});

That is:

  • I have the same generic code for different types,
  • I want the compiler to generate specific code for each type,
  • I only know which type I need at runtime, and
  • I want to execute the code for that single type only.

This feels like a switch statement with a run-time condition, but where the "cases" are generated at compile-time. In particular, this does not feel like a for_each statement at all (I am not doing anything for all elements in a vector, tuple, list, but only to a single element).

Is there a better clearer way to express/write this idiom? (E.g. use an mpl::vector instead of a std::tuple for the possible types, use something different than the for_each algorithm,...)


回答1:


I like my usual inherited lambdas trick:

I've written about this before

  • Lambda functions as base classes
  • what is the correct way to handle multiple input command differently in c++? (where it visits members of a boost::variant)

I believe I've seen Sumant Tambe use it in his more recent cpptruths.com postings.


Demonstration

Here's a demo for now. Will add some explanation later.

The most important trick applied is that I use boost::variant to hide the type code denum for us. But the principle applies even if you keep your own type discrimination logic (just requiring more coding)

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);
}

Prints

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:


I think your existing solution isn't bad. At the point of // do generic stuff instead call into other functions overloaded on type

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 you can't quite get a switch, which is a little bit faster than the if...else structure here, but not substantially and is unlikely to be noticeable for a process you run while reading a file.

Other options are all similar to this. Fusion or mpl random access containers or even std::tuple can be access with get<> but that requires a compile time index so you're building the cases up and still going through the indices with something like

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

Which could be done with recursive templates, like:

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); }

The only alternative I'm aware of would be building an array of function pointers. See Optimal way to access std::tuple element in runtime by index. I don't think you really gain anything with that solution for your case and it's harder to follow.

One advantage to your fusion::for_each solution is that it doesn't force your type indices to be continuous. As your application evolves you can add new types or remove old types easily and the code still works, which would be harder if you were trying to use the container index as your type index.




回答3:


When you say I have the same generic code for different types; is it possible to wrap it all into a function with same prototype?

If so, you can map each type_index with a std::function in order to make the compiler to generate code for each type and to have an easy way to call every function in replacement to a switch.

Switch replacement:

function_map.at(read())();

Running example:

#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:


I would say the best thing would just be to use an array of functions that do what you want to do:

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)); },
    ...
};

That array is easy to generate with the help of integer_sequence, if all your calls are really the same:

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)>{});
}

And once we have our array, regardless of which way we generate, we just need to call it:

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

Or if throwing is good enough:

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

You can't really beat array lookup on performance, although you do have indirection through std::function<void()>. This will definitely beat the fusion for_each solution, since there even if idx == 0, you're actually running through each element anyway. You would really want to use any() in that case, so you can quit early. But still faster to just use an array.




回答5:


Build an unordered_map from type_index to processing code.

Read the type_index, lookup in map, execute. Error check for missing entries.

Simple, extendible, versionable -- simply add a length header on entries (make sure it handles 64 bit lengths -- have max lower bit count length mean real length is next, which allows single bit lengths to start), and if you do not understand an entry you can skip it.



来源:https://stackoverflow.com/questions/26846299/boost-fusion-run-time-switch

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!