How to prevent manually instantiating all template types?

Viewed 157

I have a function Foo that gets two template args L3HdrType and L4HdrType. I receive a packet and parse it and then need to call the function. My code currently looks like:

ParsedPacket parsed_packet = parser.Parse(packet);
if (parsed_packet.ip_version() == IPv4 && parsed_packet.ip_proto() == TCP)
  Foo(parsed_packet.L3Header<ipv4_hdr>(), parsed_packet.L4Header<tcp_hdr>());
if (parsed_packet.ip_version() == IPv6 && parsed_packet.ip_proto() == TCP)
  Foo(parsed_packet.L3Header<ipv6_hdr>(), parsed_packet.L4Header<tcp_hdr>());
if (parsed_packet.ip_version() == IPv4 && parsed_packet.ip_proto() == UDP)
  Foo(parsed_packet.L3Header<ipv4_hdr>(), parsed_packet.L4Header<udp_hdr>());
if (parsed_packet.ip_version() == IPv6 && parsed_packet.ip_proto() == UDP)
  Foo(parsed_packet.L3Header<ipv6_hdr>(), parsed_packet.L4Header<udp_hdr>());

My question is, is there any way to reduce this code duplication? Something along the lines of:

Foo<parsed_packet.GetL3HeaderType(), parsed_packet.GetL4HeaderType()>(...);

This clearly doesn't work since the given packet's header types aren't known at compile time.

The source of duplication is the fact that two different if-statements are checking IPv4 and mapping it to ipv4_hdr. If I could in one place in the code specify that IPv4 maps to ipv4_hdr then It would make the code grow linearly with the number of options and not exponentially since I could write somehow:

if (parsed_packet.ip_version() == IPv4) {
  using L3HeaderType = ipv4_hdr;
}
...
Foo<L3HeaderType, L4HeaderType>(...)

Note, my actual code needs to support more than just 4 cases so the code in reality is much uglier than the example here since the number of if-statements grows exponentially with the number of headers.

2 Answers

If inheritance and runtime polymorphism is not an option you can decouple deduction of the two template parameters by doing some acrobatics involving lambdas:

template <typename T> struct foo {};    
template <typename T> struct bar {};

// the function to be called
template <typename A, typename B>
void foobar( foo<A> f, bar<B> b) {}

// bind the first parameter
template <typename T>
auto make_foofoo (foo<T> f) {
    return [f](auto bar){ foobar(f,bar); };
}

// select second type here
template <typename F>
void do_the_actual_call(F f, int y) {
    if (y == 1) f(bar<int>{});
    if (y == 2) f(bar<double>{});
}


int main() {
    // the "conditions"
    int x = 1;
    int y = 2;

    // select first type here
    if (x == 1) {
        auto foofoo = make_foofoo(foo<int>{});
        do_the_actual_call(foofoo,y);
    } else if (x == 2){
        auto foofoo = make_foofoo(foo<double>{});
        do_the_actual_call(foofoo,y);
    }
}

It is still duplicate code, but it scales as x + y no longer as x * y.

I've made a little library with a generic way to forward and use a variadic that holds exactly one chosen type of each "pack" of types, where the choices are made at run-time.

The library interface:

template <class ...>
struct pack {};

template <class ... Packs, class Functor>
void OneOfEachPackToFunctor (const int choice[], Functor && f);

Example:

You want to use this interface:

template <class X, class Y, class Z>
int Foo (std::string & str);

Possible types for each of Foo's arguments at compile-time:
X: X0, X1, X2, X3, X4, X5, X6, X7, X8
Y: Y0, Y1, Y2
Z: Z0, Z1, Z2, Z3, Z4, Z5

We can decide between these at run-time:

struct FooFunctor
{
    // Foo's veriable inputs:
    std::string str;

    template <class X, class Y, class Z>
    void operator () ()
    {
        Foo<X,Y,Z>(str);
    }
};

int main ()
{
    using XPack = pack<X0, X1, X2, X3, X4, X5, X6, X7, X8>;
    using YPack = pack<Y0, Y1, Y2>;
    using ZPack = pack<Z0, Z1, Z2, Z3, Z4, Z5>;

    // At some point we choose the arguments:
    int choice [] = {3, 1, 5}; // ie. X3, Y1, Z5

    FooFunctor ff;
    ff.str = "hello";

    OneOfEachPackToFunctor<XPack, YPack, ZPack>(choice, ff);
}

Live demo


Specifically for OP:

struct IP
{
    const ParsedPacket & pp;
    
    IP(const ParsedPacket & parsed_packet)
        : pp(parsed_packet)
    {}

    template <class Version, class Protocol>
    void operator () ()
    {
        Foo(pp.L3Header<Version>(), pp.L4Header<Protocol>());
    }
};

int main ()
{
    Packet p;
    // packet given at run-time includes versions/protocols:
    ParsedPacket pp(p); // (in this example we use a rand() for choices)
    
    using Versions = pack<ipv4_hdr, ipv6_hdr>;
    int chosen_version = (pp.ip_version() == IPv6);
    
    using Protocols = pack<tcp_hdr, udp_hdr>;
    int chosen_protocol = (pp.ip_proto() == UDP);
    
    int choice[] = {chosen_version, chosen_protocol};
    OneOfEachPackToFunctor<Versions, Protocols>(choice, IP(pp));
}

Live demo


Library code:

#include <utility>

template<std::size_t, std::size_t, class>
struct type_if_equal {};

template<std::size_t n, class T>
struct type_if_equal <n, n, T>
{
    using type = T;
};

////////////////////////////////////////////////

template<std::size_t, class...>
struct nth_by_multi_inheritance {};

template<std::size_t n, std::size_t ... is, class ... Ts>
struct nth_by_multi_inheritance <n, std::index_sequence<is...>, Ts...>
    : type_if_equal<n, is, Ts>...
{};

template<std::size_t n, class ... Ts>
struct nth
    : nth_by_multi_inheritance<n, std::index_sequence_for<Ts...>, Ts...>
{};

template<std::size_t n, class ... Ts>
using nth_t = typename nth<n, Ts...>::type;

////////////////////////////////////////////////

template <class...>
struct pack {};

////////////////////////////////////////////////

template <class Functor>
class OneOfEachPack
{
    template <class ... Ts>
    struct OpenPack
    {
        // Here is the base case.
        // We've "opened" all the packs and now we're
        // left only with the types chosen by the user

        static void PickType (const int *, Functor & f)
        {
            f.template operator()<Ts...>();
        }
    };

    template <class ... Ts, class ... PacksThenTs>
    struct OpenPack <pack<Ts...>, PacksThenTs...>
    {
        // We are "opening" the left-most pack, "Pack"
        //
        // PacksThenTs = [remaining "unopened" packs] then [chosen types from already "opened" packs]

        template <int N = 0>
        static void PickType (const int * choice, Functor & f)
        {
            // Each pack has multiple elements (types).
            // We consider whether the user has chosen element K

            if constexpr (N < sizeof...(Ts))
            {
                if (N < *choice)
                {
                    PickType<N+1>(choice, f);
                }
                else // (N == *choice)
                {
                    using ChosenT = nth_t<N, Ts...>;

                    OpenPack<PacksThenTs..., ChosenT>::PickType(choice + 1, f);
                }
            }
        }
    };

public:
    
    template <class ... Packs>
    static void ToFunctor (const int choice[], Functor & f)
    {
        OpenPack<Packs...>::PickType(choice, f);
    }   
};

template <class ... Packs, class Functor>
void OneOfEachPackToFunctor (const int choice[], Functor && f)
{
    OneOfEachPack<Functor>::template ToFunctor<Packs...>(choice, f);
}
Related