问题
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