Fusión de funciones con contenido similar, pero diferentes parámetros

Dado el siguiente (pseudo) código:

typedef std::map<const unsigned int, unsigned long int> ModelVector;
typedef std::vector<unsigned long int> EncryptedVector;

int test1 (const EncryptedVector &x)
{
    //compute ModelVector y
    data = kenel1(x, y);
    //compute output
}
int test2 (const EncryptedVector &xx)
{
    //compute ModelVector y
    data = kenel2(xx, y);
    //compute output
}
int test3 (const EncryptedVector &x, const EncryptedVector &xx)
{
    //compute ModelVector y
    data = kenel3(x, xx, y);
    //compute output
}
int test4 (const EncryptedVector &x, const EncryptedVector &xSquared)
{
    //compute ModelVector y
    data = kenel4(x, xSquared, y);
    //compute output
}

Debido a que las variables y y la salida se calculan de manera idéntica en las 4 funciones y dado que tengo un objeto "global" que me permite seleccionar la función de kernel adecuada a través de una declaración de cambio, me preguntaba si hay una forma más elegante de escribirlas, preferiblemente fusionándolos de alguna manera...

Por ejemplo, algo como esto (pseudocódigo) sería una opción decente:

int test (const EncryptedVector &x, const EncryptedVector &xx, const EncryptedVector &xSquared)
{
    //compute ModelVector y
    //switch (kernel) -> select the appropriate one
    //compute output
}

test (x, NULL, NULL);//test1
test (NULL, xx, NULL);//test2
test (x, xx, NULL);//test3
test (x, NULL, xSquared);//test4

O, incluso mejor, podría declarar prueba varias veces con diferentes combinaciones de parámetros, todas recurriendo a la anterior (aunque perdería la distinción semántica entre x y xx).

El problema con el enfoque anterior es que C++ no me permite pasar NULL en lugar de std::vector y creo que es mejor repetir el código 4 veces que comenzar a pasar variables por puntero en lugar de pasarlas por referencia. ..

¿Hay otra manera de hacer esto?


EDITAR: Aquí están los prototipos para las funciones del núcleo:

int kernel1 (const EncryptedVector &x, const ModelVector &y);
int kernel2 (const EncryptedVector &xx, const ModelVector &y);
int kernel3 (const EncryptedVector &x, const EncryptedVector &xx, const ModelVector &y);
int kernel4 (const EncryptedVector &x, const EncryptedVector &xSquared, const ModelVector &y);

preguntado el 12 de junio de 12 a las 16:06

Tangencial -- Las funciones libres no pueden ser const. -

Joder, tienes razón. Los tengo dentro de una clase :D -

¿Cómo sería el kernel ser seleccionado en su código propuesto? -

¿Y puede proporcionar prototipos para los 4 diferentes? kernel funciones? -

@JohnDibling Bueno, dado que estas funciones son en realidad métodos dentro de una clase (no quería complicar demasiado el pseudocódigo), tengo un miembro privado que contiene el kernel apropiado, que se inicializa en el constructor. -

2 Respuestas

tus 4 originales test métodos seleccionan un comportamiento diferente (por ejemplo, un diferente kernel se llama la función) basado no en el tipos de los argumentos pasados, pero por la semántica de los argumentos pasados. Sin mencionar el hecho de que tienen nombres completamente diferentes.

Si de alguna manera pudiera cambiar esta parametrización de una semántica a una de un tipo, podría usar su método propuesto de combinar los 4 métodos en uno.

Esto significa tener una función con los 3 parámetros, que es exactamente lo que ha propuesto:

int test (const EncryptedVector &x, const EncryptedVector &xx, const EncryptedVector &xSquared)

...el problema, por supuesto, es que no puedes pasar NULL por referencia. Tienes que pasar un vector. Podría pasar un vector temporal vacío, como este:

test (x, EncryptedVector(), EncryptedVector());  //test1

...y luego seleccione el método internamente basado en el empty() estado del pasado en vector:

int test (const EncryptedVector &x, const EncryptedVector &xx, const EncryptedVector &xSquared)
{
    //compute ModelVector y
    //switch (kernel) -> select the appropriate one
      if( xx.empty() && xSquared.empty() )
      {
        // kenel1 method
      }
      else if( x.empty() && xSquared.empty() )
      {
        // kenel2 method
      }
      else if( ... etc ... )

    //compute output
}

Y tal vez para sus usos esto sea suficiente, pero lo anterior plantea al menos un par de problemas nuevos.

Uno es la cadena de if declaraciones. Esto introduce complejidad, tanto en factores de tiempo de ejecución, y quizás más importante en términos de mantenibilidad. Sé que no me gustaría mantener 4 páginas de if Declaraciones.

Otros problemas incluyen la invocación torpe del método con vacío vectors, y el hecho de que todavía puedes llamar test con una combinación inválida de vectors.

Entonces, volviendo a usar una selección de comportamiento basada en tipos en lugar de una semántica. ¿Qué sucede si no especificó un tipo concreto para test()los parámetros de , pero un template ¿en lugar?

template<class VectorX, class VectorXX, class VectorXSquared>
int test(const VectorX& x, const VectorXX& xx, const VectorXSquared& xSquared);

Ahora también puede proporcionar un especial NullVector tipo:

class NullVector {}; // Empty Class

... y luego especializar explícitamente el test función para cada uno de sus casos de uso válidos (arriba, ha enumerado 4 casos de uso válidos).

Invocación de test ahora se convierte en algo como esto:

test(x,NullVector(),NullVector());  // First Use Case

Esto también tiene un beneficio adicional. Si no proporciona una implementación para la versión no especializada de test, cualquier intento de llamar test con parámetros no válidos no se podrá compilar ni vincular. Por ejemplo, en sus casos de uso enumerados, ninguno de ellos toma 3 válidos EncryptedVector objetos, por lo que esto debería fallar al compilar:

test(x, xx, xSquared);

... que, de hecho, lo hará si no proporciona una implementación no especializada de test.

OK, eso es mucho hablar y tal vez no estaba todo bien explicado. Así que aquí hay una muestra completa que espero ayude a ilustrar de lo que estoy hablando:

#include <vector>
#include <string>
using namespace std;

typedef vector<string> EncryptedVector;

class NullVector{}; // Empty Class

int kernel1(const EncryptedVector& x)
{
    return 1;
}

int kernel2(const EncryptedVector& xx)
{
    return 2;
}

int kernel3(const EncryptedVector& x, const EncryptedVector& xx)
{
    return 3;
}

int kernel4(const EncryptedVector& x, const EncryptedVector& xSquared)
{
    return 4;
}

template<class VectorX, class VectorXX, class VectorXSquared> 
int test(const VectorX& x, const VectorXX& xx, const VectorXSquared& xSquared);

template<> int test<>(const EncryptedVector& x, const NullVector&, const NullVector&)
{
    return kernel1(x);
}

template<> int test<>(const NullVector&, const EncryptedVector& xx, const NullVector&)
{
    return kernel2(xx);
}

template<> int test<>(const EncryptedVector& x, const EncryptedVector& xx, const NullVector&)
{
    return kernel3(x, xx);
}

template<> int test<>(const EncryptedVector &x, const NullVector&, const EncryptedVector &xSquared)
{
    return kernel4(x,xSquared);
}

int main()
{
    EncryptedVector x, xx, xSquared;  // each somehow populated

    ///*** VALID USE-CASES ***///
    test(x,NullVector(),NullVector());
    test(NullVector(),xx,NullVector());
    test(x,xx,NullVector());
    test(x,NullVector(),xSquared);

    ///*** INVALID USE CASES WILL FAIL TO COMPILE&LINK ***///
    test(x,xx,xSquared);

}

Respondido el 12 de junio de 12 a las 17:06

Gracias Juan Esa es una muy buena solución para mi problema de diseño. - Mihai Todor

Eres bienvenido. Siempre es divertido resolver problemas un poco más grandes que simplemente "¿por qué obtuve un segafult aquí?" :) - Juan Dibling

Sí sé lo que quieres decir. Quiero convertirme en un arquitecto de software, a largo plazo :) y he estado golpeándome la cabeza contra la pared con esto todo el día, porque soy nuevo en C ++ (probablemente todavía seré un n00b para el el próximo año más o menos, ya que es un lenguaje tan complejo) y me cuesta mucho acostumbrarme a las plantillas... - Mihai Todor

Podrías enviar un objeto (se llama functor, si no me equivoco) que contenga los parámetros adicionales y tenga una función que calcule el kernel (¿kenel?).

// Main function, looks more generic now
template <class F>
int test1234(F functor)
{
    //compute ModelVector y
    data = functor(y);
    //compute output
}

...

// Functors involve some tedious coding, but at least your main calculation is clean
struct functor_for_kernel1
{
    const EncryptedVector &x;
    functor_for_kernel1(const EncryptedVector &x): x(x) {}
    int operator(const ModelVector &y) {return kernel1(x, y);
};

struct functor_for_kernel2 // easy
...

struct functor_for_kernel3
{
    const EncryptedVector &x, const EncryptedVector &xx;
    functor_for_kernel3(const EncryptedVector &x, const EncryptedVector &xx): x(x), xx(xx) {}
    int operator()(const ModelVector &y) {return kernel3(x, xx, y);
};

struct functor_for_kernel4 // easy
...

// Usage of the unified function
test1234(functor_for_kernel1(x));
test1234(functor_for_kernel2(xx));
test1234(functor_for_kernel3(x, xx));
test1234(functor_for_kernel4(x, xx));

Respondido el 12 de junio de 12 a las 17:06

Aunque este enfoque también es válido, prefiero seguir la sugerencia de John, porque es más limpio e implica menos codificación. - Mihai Todor

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.