Combination explosion of an enum value (729 combinations...)
Asked Answered
G

2

1

im facing a problem in which I have to generate a huge amount of code, all of it fairly similar, and I want to know if there is any way to templatized.

Lets say I have a structure of this type

template <typename ...NodeKindTs>
struct Element{
std::tuple<NodeKindTs...> nodes;
}

I have a vector of ints which related a Node with another, and a vector of an enum which says which kind of Node is each. The kind can be A, B or C.

enum class Kind {A,B,C};
std::vector<int> relationShip;
std::vector<Kind> kind;

For example, if I have

relationShip = {1,2,-1};
kind = {A,B,A}

would mean that the first node is of kind A and is related to the second node, which is of kind B. You get it.

Now, I have to create Elements and insert them into a vector depending on the NodeKind that each node and the relation ship. This Elements are templatized by up to 6 NodeKinds. For solving this, I need a huge if that check the Kind of each node and then call the Element ctor.

For the 2 NodeKinds case, this means doing something like

if (node1.type == A && node2.type == A) {
auto &simNode1 = containerOfAs(node1.id);
auto &smiNode2 = containerOfAs(node2.id);
insertElement(elementFactory(simNode1, simNode2));
}
if (node1.type == A && node2.type == C) 
{
auto &simNode1 = containerOfAs(node1.id);
auto &smiNode2 = containerOfCs(node2.id);
insertElement(elementFactory(simNode1, simNode2));
}
if (node1.type == B && node2.type == C) 
{
auto &simNode1 = containerOfBs(node1.id);
auto &smiNode2 = containerOfCs(node2.id);
insertElement(elementFactory(simNode1, simNode2));
}
...

inserElement is a meta-function I have create which insert the element into the container if fits in from a list of containers.

For this 2 case, this requires up to 9 ifs. For the 3 case it requires 27 ifs and for the 6 case it requires 729 ifs. I really dont want to code them.

Any idea on how I could solve this?

Thanks!

Gaggle answered 4/3, 2019 at 17:23 Comment(5)
Sounds sort of like how a similar issue was solved in Boost Units. boost.org/doc/libs/1_69_0/doc/html/boost_units.htmlHooky
You can still use std::variant to let it doing the combination for you... I'm afraid that it will slow down compilation though.Scorper
@Scorper how would you use std::variant to solve this. Ive been thinking about it, but havent found an answer.Gaggle
@Hooky Ive been looking at the documentation of Boost.Units, but I havent found a similar problem. Can you point me a little bit more? Thanks!Gaggle
Boost Units uses parameterized templates for various permutations of the SI units. Maybe I've misunderstood the problem you are facing.Hooky
S
1

With std::variant, you might have something like:

std::variant<std::reference_wrapper<NodeA>,
             std::reference_wrapper<NodeB>,
             std::reference_wrapper<NodeC>> getNodeAsVariant(Kind kind, int id)
{
    switch (kind) {
        case Kind::A: return containerOfAs(id);
        case Kind::B: return containerOfBs(id);
        case Kind::C: return containerOfCs(id);
    }
    throw std::runtime_error("Invalid kind");
}

And then

auto v1 = getNodeAsVariant(node1.type, node1.id);
auto v2 = getNodeAsVariant(node2.type, node2.id);
auto v3 = getNodeAsVariant(node3.type, node3.id);
auto v4 = getNodeAsVariant(node4.type, node4.id);
auto v5 = getNodeAsVariant(node5.type, node5.id);
auto v6 = getNodeAsVariant(node6.type, node6.id);

// r1, .., r6 would be the correct reference_wrapper<T>
std::visit([](auto r1, auto r2, auto r3, auto r4, auto r5, auto r6) {
         insertElement(elementFactory(r1/*.get()*/, r2, r3, r4, r5, r6));
    }, v1, v2, v3, v4, v5, v6);

So std::visit would generate the 729 overloads for you.

Scorper answered 4/3, 2019 at 19:59 Comment(7)
The element factory receives a reference to the node (in this case, a reference_wrapper). Is the variant converted implicitly to its underlying type? How does it know it has to be converted to type NodeA or type NodeB?Gaggle
std::visit does the magic and according to v1.index(), calls the correct overloads corresponding to f(std.get<v1.index()>(v1), std.get<v2.index()>(v2)) (std.get<v1.index()>(v1) is invalid as v1.index() is runtime, but I hope you get the idea).Scorper
That's nice. I thought on a solution like that, but I wasn't sure it works, as I thought you need to know at compile time the overload. Im testing it right now with the 2 type case. Do you know if it would work with Boost.Variant? Thanks!Gaggle
boost::variant has boost:: apply_visitor.(but it seems you need to inherit from boost::static_visitor so creating dedicated struct instead of lambda).Scorper
Still not working. I have created the visitor lambda and Im passing the two variants of the 2 type case. I get a compiler error saying there's no overload of the factory receiving variants. My factory is a template which receives two references of T1 and T2 (for the 2 type case) and return the Element<T1, T2>.Gaggle
Looks fine in godbolt.org. Compiles and runs as expected. Ill double check my production code to see where's the error. Thanks a lot!Gaggle
Demo (only 3 varaibles, as using 6 variables takes too much time for online compilers).Scorper
S
1

Maybe something like this can be used as a starting point (I may have misunderstood the question and there are probably ways to make it shorter):

#include <iostream>

enum class Kind { A, B, C };

std::ostream& operator<<(std::ostream& os, Kind k) {
    switch (k) {
        case Kind::A: return os << "A";
        case Kind::B: return os << "B";
        case Kind::C: return os << "C";
    }
    return os << "Unknown";
}


template<typename F>
void dispatch(Kind k, F f) {
    switch (k) {
        case Kind::A: f.template call<Kind::A>(); return;
        case Kind::B: f.template call<Kind::B>(); return;
        case Kind::C: f.template call<Kind::C>(); return;
    }
    abort();
}

template<Kind k1>
struct handle2 {
    template<Kind k2>
    void call() {
        std::cout << "k1=" << k1 << " k2=" << k2 << "\n";
        // Do your thing with k1 and k2 here
    }
};

struct handle1 {
    Kind k2;
    template<Kind k1>
    void call() {
        dispatch(k2, handle2<k1>{});
    }
};

void handle(Kind k1, Kind k2) {
    dispatch(k1, handle1{k2});
}

int main() {
    handle(Kind::C, Kind::B);
}
Selfcontent answered 4/3, 2019 at 18:9 Comment(3)
I cant see how this solves the problem. Your dispatch function takes two enum values. As I said, I can have up to 6 values. Also, you dont use NodeKind for anything, so how do you create the Element<> type?Gaggle
Well, like I wrote I may have misunderstood your question, so I only sketched what I thought would be a solution for 2 values. It's still not 100% clear to me what you need, but I'd create the Element in handle2.call (possibly passing references to the needed context along the way). If I'm totally off base just let me know and I'll delete the answer.Selfcontent
I need to create an Element<T1, T1, T1> if I have Enum1, Enum1, Enum1 combination, Element<T2, T1, T2> if I have Enum2, Enum1, Enum2 and so on. For two types is easy, its only combinations (given 3 values for the enum). For more types, it becomes too much typing and i was looking a way to generate the Element automatically. Can you try to depict a 3 type system to see if I can understand better how to extend it? Thanks!Gaggle
S
1

With std::variant, you might have something like:

std::variant<std::reference_wrapper<NodeA>,
             std::reference_wrapper<NodeB>,
             std::reference_wrapper<NodeC>> getNodeAsVariant(Kind kind, int id)
{
    switch (kind) {
        case Kind::A: return containerOfAs(id);
        case Kind::B: return containerOfBs(id);
        case Kind::C: return containerOfCs(id);
    }
    throw std::runtime_error("Invalid kind");
}

And then

auto v1 = getNodeAsVariant(node1.type, node1.id);
auto v2 = getNodeAsVariant(node2.type, node2.id);
auto v3 = getNodeAsVariant(node3.type, node3.id);
auto v4 = getNodeAsVariant(node4.type, node4.id);
auto v5 = getNodeAsVariant(node5.type, node5.id);
auto v6 = getNodeAsVariant(node6.type, node6.id);

// r1, .., r6 would be the correct reference_wrapper<T>
std::visit([](auto r1, auto r2, auto r3, auto r4, auto r5, auto r6) {
         insertElement(elementFactory(r1/*.get()*/, r2, r3, r4, r5, r6));
    }, v1, v2, v3, v4, v5, v6);

So std::visit would generate the 729 overloads for you.

Scorper answered 4/3, 2019 at 19:59 Comment(7)
The element factory receives a reference to the node (in this case, a reference_wrapper). Is the variant converted implicitly to its underlying type? How does it know it has to be converted to type NodeA or type NodeB?Gaggle
std::visit does the magic and according to v1.index(), calls the correct overloads corresponding to f(std.get<v1.index()>(v1), std.get<v2.index()>(v2)) (std.get<v1.index()>(v1) is invalid as v1.index() is runtime, but I hope you get the idea).Scorper
That's nice. I thought on a solution like that, but I wasn't sure it works, as I thought you need to know at compile time the overload. Im testing it right now with the 2 type case. Do you know if it would work with Boost.Variant? Thanks!Gaggle
boost::variant has boost:: apply_visitor.(but it seems you need to inherit from boost::static_visitor so creating dedicated struct instead of lambda).Scorper
Still not working. I have created the visitor lambda and Im passing the two variants of the 2 type case. I get a compiler error saying there's no overload of the factory receiving variants. My factory is a template which receives two references of T1 and T2 (for the 2 type case) and return the Element<T1, T2>.Gaggle
Looks fine in godbolt.org. Compiles and runs as expected. Ill double check my production code to see where's the error. Thanks a lot!Gaggle
Demo (only 3 varaibles, as using 6 variables takes too much time for online compilers).Scorper

© 2022 - 2024 — McMap. All rights reserved.