Tengo una función Foo que obtiene dos argumentos de plantilla L3HdrType y L4HdrType. Recibo un paquete y lo analizo y luego necesito llamar a la función. Mi código actualmente se ve así:

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

Mi pregunta es, ¿hay alguna forma de reducir esta duplicación de código? Algo parecido a:

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

Esto claramente no funciona ya que los tipos de encabezado del paquete dado no se conocen en el momento de la compilación.

La fuente de la duplicación es el hecho de que dos sentencias if diferentes verifican IPv4 y lo mapean a ipv4_hdr. Si pudiera en un lugar del código especificar que IPv4 se asigna a ipv4_hdr, entonces haría que el código creciera linealmente con la cantidad de opciones y no exponencialmente, ya que podría escribir de alguna manera:

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

Tenga en cuenta que mi código real debe admitir más de 4 casos, por lo que el código en realidad es mucho más feo que el ejemplo aquí, ya que el número de declaraciones if crece exponencialmente con el número de encabezados.

5
Benjy Kessler 8 oct. 2020 a las 16:22

2 respuestas

La mejor respuesta

Si la herencia y el polimorfismo en tiempo de ejecución no es una opción, puede desacoplar la deducción de los dos parámetros de la plantilla haciendo algunas acrobacias que involucren 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);
    }
}

Todavía es un código duplicado, pero se escala como x + y ya no como x * y.

4
largest_prime_is_463035818 8 oct. 2020 a las 13:57

Aquí hay una solución alternativa que funcionará sin importar que tenga muchas opciones de parámetros que deba hacer, y no requiere el paso en tiempo de ejecución de objetos lambda / function.


Aquí está el material genérico:

#include <type_traits>

// We choose which arguments (Args...)
// to send to the Call method:

template <int N, int N_MAX, typename Caller, typename ... Args>
std::enable_if_t<N == N_MAX>
ChooseTemplateArgumentsRecursive (const bool[])
{
    Caller::template Call<Args...>();
}

template <int N, int N_MAX, typename Caller, typename CandidateArg1, typename CandidateArg2, typename ... Args>
std::enable_if_t<N != N_MAX>
ChooseTemplateArgumentsRecursive (const bool choice[])
{
    if (choice[N])
        ChooseTemplateArgumentsRecursive<N+1, N_MAX, Caller, Args..., CandidateArg1>(choice);
        
    else
        ChooseTemplateArgumentsRecursive<N+1, N_MAX, Caller, Args..., CandidateArg2>(choice);
}

// You only need to call this function:

template <typename Caller, typename ... CandidateArgs>
void ChooseTemplateArguments (const bool choice[])
{
    constexpr int N_MAX = sizeof...(CandidateArgs) / 2;

    ChooseTemplateArgumentsRecursive<0, N_MAX, Caller, CandidateArgs...>(choice);
}

Lo anterior funciona con c++14 o posterior. Si solo tiene acceso a c++11, cambie ambos:

std::enable_if_t</*expression*/>

...a:

typename std::enable_if</*expression*/>::type

Cómo usarlo:

ChooseTemplateArguments<CallerFoo, ipv4_hdr, ipv6_hdr, tcp_hdr, udp_hdr>(choice);

Así que aquí tenemos una matriz bool que representa los argumentos que ha elegido. Entonces, si choice[0] es true, entonces ipv4_hdr se incluirá en el paquete de parámetros que se envía a Caller::Call; de lo contrario, ipv6_hdr se incluye. Del mismo modo, si choice[1] es true, entonces tcp_hdr se incluye en el paquete de parámetros enviado, etc. En el sitio Call, tiene el paquete de parámetros que eligió y puede hacerlo. con él como más te guste. Depende de usted configurar el sitio Call de acuerdo con sus necesidades.

Para su ejemplo, se vería así:

struct CallerFoo
{
    static ParsedPacket parsed_packet;

    template <typename IpV, typename IpP>
    static void Call ()
    {
        Foo(parsed_packet.L3Header<IpV>(), parsed_packet.L4Header<IpP>());
    }
};
ParsedPacket CallerFoo::parsed_packet;

Call debe ser nombrado como tal y debe ser un miembro estático. Todo lo demás puede ser como lo desee.

El paquete de parámetros mantiene su orden original.


Esta opción no es particularmente valiosa en el ejemplo, pero como dijiste:

[...] mi código real necesita admitir más de 4 casos, por lo que el código en realidad es mucho más feo que el ejemplo aquí ya que el número de declaraciones if crece exponencialmente con el número de encabezados.

... por lo que esta solución aquí debería proporcionar una buena escalabilidad / mantenibilidad en su caso del mundo real.


Aquí tienes un ejemplo completo de trabajo.

0
Elliott 20 oct. 2020 a las 05:30