2009-07-23 10 views
176

Estoy buscando las reglas que implican pasar funciones de plantillas de C++ como argumentos.Función pasada como argumento de plantilla

Esto es apoyado por C++, como se muestra mediante un ejemplo aquí:

#include <iostream> 

void add1(int &v) 
{ 
    v+=1; 
} 

void add2(int &v) 
{ 
    v+=2; 
} 

template <void (*T)(int &)> 
void doOperation() 
{ 
    int temp=0; 
    T(temp); 
    std::cout << "Result is " << temp << std::endl; 
} 

int main() 
{ 
    doOperation<add1>(); 
    doOperation<add2>(); 
} 

aprendizaje de esta técnica es difícil, sin embargo. Googling for "function as a template argument" no conduce a mucho. Y el clásico C++ Templates The Complete Guide sorprendentemente tampoco lo discute (al menos no desde mi búsqueda).

Las preguntas que tengo son si esto es válido C++ (o simplemente una extensión ampliamente soportada).

Además, ¿hay alguna manera de permitir que un funtor con la misma firma se use indistintamente con funciones explícitas durante este tipo de invocación de plantilla?

El siguiente no funciona en el programa anterior, al menos en Visual C++, porque la sintaxis es obviamente incorrecta. Sería bueno poder cambiar una función para un funtor y viceversa, similar a la forma en que puede pasar un puntero a función o un funtor al algoritmo std :: sort si quiere definir una operación de comparación personalizada.

struct add3 { 
     void operator() (int &v) {v+=3;} 
    }; 
... 

    doOperation<add3>(); 

¡Se apreciarán los punteros a un enlace web o dos, o una página en el libro de Plantillas C++!

+0

¿Cuál es el beneficio de una función como argumento de plantilla? ¿No se usaría el tipo de devolución en el tipo de plantilla? – DaClown

Respuesta

98

Sí, es válido.

En cuanto a lo que es trabajar con palabras funcionales, así, la solución habitual es algo como esto en su lugar:

template <typename F> 
void doOperation(F f) 
{ 
    int temp=0; 
    f(temp); 
    std::cout << "Result is " << temp << std::endl; 
} 

que ahora se puede llamar ya sea como:

doOperation(add2); 
doOperation(add3()); 

El problema con esto es eso si hace complicado para el compilador alinear la llamada a add2, ya que todo lo que el compilador sabe es que un puntero a la función tipo void (*)(int &) se está pasando a doOperation. (Pero add3, al ser un funtor, se puede insertar fácilmente. Aquí, el compilador sabe que un objeto de tipo add3 se pasa a la función, lo que significa que la función a llamar es add3::operator(), y no solo un puntero de función desconocida)

+17

Ahora aquí hay una pregunta interesante. Cuando se pasa un nombre de función, NO es como si hubiera un puntero de función involucrado. Es una función explícita, dada en tiempo de compilación. Entonces el compilador sabe exactamente lo que tiene en tiempo de compilación. – SPWorley

+0

¿Se podría usar una sobrecarga de plantilla para hacer que el puntero de la función y las cajas de los funtores tengan la misma sintaxis? ¿Habría algún beneficio de rendimiento? Especialmente para los funtores apátridas que efectivamente no tienen instancias y, por lo tanto, queremos evitar cualquier caso en el que el compilador pueda detener el alineamiento y la optimización. – SPWorley

+1

Existe una ventaja al usar funtores sobre los punteros de función. El funtor se puede instanciar dentro de la clase y, por lo tanto, proporciona más operatividad al compilador para optimizaciones (como en línea). El compilador tendría dificultades para optimizar una llamada sobre un puntero de función. –

1

El motivo por el que su ejemplo de functor no funciona es que necesita una instancia para invocar el operator().

8

En su plantilla

template <void (*T)(int &)> 
void doOperation() 

El parámetro T es un parámetro de plantilla no tipo. Esto significa que el comportamiento de la función de plantilla cambia con el valor del parámetro (que debe corregirse en tiempo de compilación, que son las constantes del puntero a la función).

Si desea algo que funcione tanto con objetos de funciones como con parámetros de funciones, necesita una plantilla tipada. Sin embargo, cuando hace esto, también necesita proporcionar una instancia de objeto (instancia de objeto de función o un puntero de función) a la función en tiempo de ejecución.

template <class T> 
void doOperation(T t) 
{ 
    int temp=0; 
    t(temp); 
    std::cout << "Result is " << temp << std::endl; 
} 

Hay algunas consideraciones de rendimiento menores. Esta nueva versión puede ser menos eficiente con argumentos de puntero a función ya que el puntero de función particular solo se desactiva y se llama en tiempo de ejecución mientras que su plantilla de puntero de función se puede optimizar (posiblemente la llamada de función incorporada) en función del puntero de función particular utilizado. Los objetos de función a menudo se pueden expandir de manera muy eficiente con la plantilla tipada, aunque el operator() particular está completamente determinado por el tipo de objeto de función.

0

Editar: Pasar al operador como referencia no funciona. Para simplificar, entiéndelo como un puntero a la función. Usted solo envía el puntero, no una referencia. Creo que estás tratando de escribir algo como esto.

struct Square 
{ 
    double operator()(double number) { return number * number; } 
}; 

template <class Function> 
double integrate(Function f, double a, double b, unsigned int intervals) 
{ 
    double delta = (b - a)/intervals, sum = 0.0; 

    while(a < b) 
    { 
     sum += f(a) * delta; 
     a += delta; 
    } 

    return sum; 
} 

. .

std::cout << "interval : " << i << tab << tab << "intgeration = " 
<< integrate(Square(), 0.0, 1.0, 10) << std::endl; 
53

Los parámetros de la plantilla se pueden parametrizar por tipo (nombre de tipo T) o por valor (int X).

La forma "tradicional" en C++ de crear plantillas en un código es usar un funtor, es decir, el código está en un objeto, y el objeto le da al código un tipo único.

Cuando se trabaja con funciones tradicionales, esta técnica no funciona bien, porque un cambio en el tipo no indica una función específica, sino que especifica únicamente la firma de muchas funciones posibles. Entonces:

template<typename OP> 
int do_op(int a, int b, OP op) 
{ 
    return op(a,b); 
} 
int add(int a, int b) { return a + b; } 
... 

int c = do_op(4,5,add); 

No es equivalente a functor. En este ejemplo, do_op se crea una instancia para todos los punteros de función cuya firma es int X (int, int). El compilador tendría que ser bastante agresivo para alinear completamente este caso. (. Yo no lo descartaría, aunque, como la optimización del compilador ha conseguido bastante avanzado)

Una forma de decir que este código no acaba de hacer lo que queremos es:

int (* func_ptr)(int, int) = add; 
int c = do_op(4,5,func_ptr); 

sigue siendo legal , y claramente esto no se está incorporando. Para obtener la alineación completa, necesitamos plantilla por valor, por lo que la función está completamente disponible en la plantilla.

typedef int(*binary_int_op)(int, int); // signature for all valid template params 
template<binary_int_op op> 
int do_op(int a, int b) 
{ 
return op(a,b); 
} 
int add(int a, int b) { return a + b; } 
... 
int c = do_op<add>(4,5); 

En este caso, cada versión instanciada de do_op se instancia con una función específica ya disponible. Por lo tanto, esperamos que el código de do_op se parezca mucho a "return a + b". (Programadores de Lisp, detienen su smirking!)

También podemos confirmar que esto está más cerca de lo que queremos, porque esto:

int (* func_ptr)(int,int) = add; 
int c = do_op<func_ptr>(4,5); 

fallará para compilar. GCC dice: "error: 'func_ptr' no puede aparecer en una expresión constante. En otras palabras, no puedo expandir completamente do_op porque no me has dado suficiente información en el compilador para saber cuál es nuestra operación.

Por lo tanto, si el segundo ejemplo está realmente delimitando por completo nuestra operación, y la primera no, ¿de qué sirve la plantilla? ¿Qué está haciendo? La respuesta es: escriba la coerción.Este riff en el primer ejemplo funcionará:

template<typename OP> 
int do_op(int a, int b, OP op) { return op(a,b); } 
float fadd(float a, float b) { return a+b; } 
... 
int c = do_op(4,5,fadd); 

¡Ese ejemplo funcionará! (No estoy sugiriendo que sea bueno en C++, pero ...) Lo que sucedió es que do_op ha sido modelado alrededor de las firmas de las distintas funciones, y cada creación de instancias por separado escribirá un código de coerción diferente. Así que el código instanciada para do_op con FADD se ve algo como:

convert a and b from int to float. 
call the function ptr op with float a and float b. 
convert the result back to int and return it. 

En comparación, nuestro caso por valor requiere una correspondencia exacta en los argumentos de la función.

+1

Ver http://stackoverflow.com/questions/13674935/why-is-it-clear-that-a-template-function-instantiation-will-not-be-inlined para una pregunta de seguimiento en respuesta directa a la observación aquí que 'int c = do_op (4,5, func_ptr);' es "claramente no se inline". –

+0

Vea aquí un ejemplo de esto: http://stackoverflow.com/questions/4860762/c-can-compilers-inline-a-function-pointer Parece que los compiladores se están volviendo bastante inteligentes en estos días. – BigSandwich

7

Los punteros de función se pueden pasar como parámetros de plantilla, y this is part of standard C++ . Sin embargo, en la plantilla se declaran y utilizan como funciones en lugar de puntero a función. En la plantilla instanciación se pasa la dirección de la función en lugar de solo el nombre.

Por ejemplo:

int i; 


void add1(int& i) { i += 1; } 

template<void op(int&)> 
void do_op_fn_ptr_tpl(int& i) { op(i); } 

i = 0; 
do_op_fn_ptr_tpl<&add1>(i); 

Si desea pasar un tipo funtor como un argumento de plantilla:

struct add2_t { 
    void operator()(int& i) { i += 2; } 
}; 

template<typename op> 
void do_op_fntr_tpl(int& i) { 
    op o; 
    o(i); 
} 

i = 0; 
do_op_fntr_tpl<add2_t>(i); 

Varias respuestas pasan una instancia funtor como argumento:

template<typename op> 
void do_op_fntr_arg(int& i, op o) { o(i); } 

i = 0; 
add2_t add2; 

// This has the advantage of looking identical whether 
// you pass a functor or a free function: 
do_op_fntr_arg(i, add1); 
do_op_fntr_arg(i, add2); 

Lo más cerca que puede llegar a este aspecto uniforme con un argumento de plantilla es definir do_op dos veces, una vez con un parámetro sin tipo y una con un parámetro de tipo.

// non-type (function pointer) template parameter 
template<void op(int&)> 
void do_op(int& i) { op(i); } 

// type (functor class) template parameter 
template<typename op> 
void do_op(int& i) { 
    op o; 
    o(i); 
} 

i = 0; 
do_op<&add1>(i); // still need address-of operator in the function pointer case. 
do_op<add2_t>(i); 

Honestamente, realmente esperaba esto no para compilar, pero funcionó para mí con gcc-4.8 y Visual Studio 2013.

Cuestiones relacionadas