Boost.Fusion运行时开关

13

我正在从文件中读取对象的类型:

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语句,但“case”是在编译时生成的。特别地,这完全不像for_each语句(我并没有为向量、元组、列表中的所有元素做任何事情,而仅仅针对单个元素进行操作)。

有没有更好、更清晰的表达/编写这种习惯用法的方法?(例如,使用mpl::vector代替std::tuple来表示可能的类型,使用与for_each算法不同的东西等等…)


1
你的例子让我想起了曾经提出的(而且我相信被拒绝的)Boost.Switch (Code)。 我认为 这个 做了类似于你想要的东西,但我可能误解了。 - llonesmiz
@cv_and_he:针对这个问题,Boost.Switch是一个很好的解决方案。请注意,该库并未被拒绝,但在2008年1月被临时接受后似乎已被放弃。:-[您可以考虑将其作为答案发布。 - ildjarn
1
type_index 是否是连续的并从零开始? - Yakk - Adam Nevraumont
听起来你想要的正是 Boost.Variant 提供的,再加上一个填充此 variant 的工厂函数。 - Sebastian Redl
5个回答

8

我喜欢我的常规继承lambda技巧:

我之前已经写过这个。

我相信我曾看到Sumant Tambe在他最近的cpptruths.com帖子中使用过它。


演示

这里有一个演示,稍后会添加一些说明。

最重要的技巧是我使用boost::variant来隐藏类型代码枚举。但是,即使您保留自己的类型判别逻辑(只需要更多的编码),该原则也适用。

立即测试

#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

但是这种方式是基于类型进行区分的,而原帖中的值只有在运行时才能确定,不是吗? - ildjarn
@ildjarn 嗯。看起来你是对的。我让自己过于依赖扫描现有答案了。正在修复... - sehe
@ildjarn,我已经更新了演示代码,使其更相关。稍后我会解释的 - 但现在我得走了 :) - sehe
啊,我明白了;我猜想这就是你要走的方向。+1,在我看来,这是迄今为止最明智的解决方案。 - ildjarn
OP所询问的部分难道不正是boost::variant所做的吗? - Yakk - Adam Nevraumont
显示剩余6条评论

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

我认为在这里你不能完全使用比 if...else 结构略快但不太明显的switch,对于在读取文件时运行的过程不容易被注意到。
其他选项都与此类似。 Fusion 或 mpl 随机访问容器甚至 std::tuple 可以使用 get<> 进行访问,但这需要编译时索引,因此您需要构建用于 case 的索引,并且仍需通过某些方式遍历这些索引。
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的一个优点是它不强制你的类型索引连续。随着应用程序的发展,您可以轻松地添加新类型或删除旧类型,代码仍然有效,如果您试图将容器索引用作类型索引,则会更加困难。

1

当您说“我有相同的通用代码适用于不同的类型”时,是否可能将其全部包装到具有相同原型的函数中?

如果可以,您可以使用std::function将每个type_index映射,以便编译器为每个类型生成代码,并且可以轻松调用每个函数来替换开关。

开关替换:

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

0
我认为最好的方法就是使用一个函数数组来完成你想要做的事情:
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(),这样你就可以提前退出了。但是仍然比使用数组更快。


0

构建一个从 type_index 到处理代码的无序映射(unordered_map)。

读取 type_index,在映射中查找并执行。对缺少条目进行错误检查。

简单、可扩展、可版本化——只需在条目上添加长度头(确保处理 64 位长度——最小的低位计数长度意味着实际长度是下一个数字,从而允许单个位长度开始),如果不理解某个条目,可以跳过它。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接