c++ - template - Función pasada como argumento de plantilla
qué es una plantilla en c++ (6)
Estoy buscando las reglas que involucran pasar las funciones de las plantillas de C ++ como argumentos.
Esto es compatible con C ++ como se muestra en 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>();
}
Sin embargo, aprender sobre esta técnica es difícil. Buscar en Google para "funcionar como un argumento de plantilla" no lleva a mucho. Y las plantillas clásicas de C ++. La Guía completa, sorprendentemente, tampoco lo discute (al menos no de mi búsqueda).
Las preguntas que tengo son si este es C ++ válido (o solo una extensión ampliamente admitida).
Además, ¿hay una manera de permitir que un funtor con la misma firma se use de manera intercambiable con funciones explícitas durante este tipo de invocación de plantilla?
Lo 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 functor y viceversa, de manera similar a la forma en que se puede pasar un puntero o funtor de función al algoritmo std :: sort si desea definir una operación de comparación personalizada.
struct add3 {
void operator() (int &v) {v+=3;}
};
...
doOperation<add3>();
¡Los punteros a un enlace web o dos, o una página en el libro de Plantillas de C ++ serían apreciados!
Edición: pasar el operador como referencia no funciona. Por simplicidad, entiéndelo como un puntero a función. Usted acaba de enviar 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;
En tu 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 fijarse en el momento de la compilación, qué función son las constantes de puntero).
Si desea algo que funcione con los objetos de función y los parámetros de función, necesita una plantilla escrita. Sin embargo, al hacer esto, también debe proporcionar una instancia de objeto (ya sea una 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 menores de rendimiento. Esta nueva versión puede ser menos eficiente con argumentos de puntero de función, ya que el puntero de función particular solo se elimina de referencia y se invoca en el tiempo de ejecución, mientras que su plantilla de puntero de función se puede optimizar (posiblemente la llamada de función en línea) 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 escrita, aunque como el operator()
particular operator()
está completamente determinado por el tipo del objeto de función.
La razón por la que su ejemplo de functor no funciona es que necesita una instancia para invocar al operator()
.
Los parámetros de la plantilla se pueden parametrizar por tipo (nombre de tipo T) o por valor (int X).
La forma "tradicional" de C ++ de crear plantillas de código es utilizar un functor, es decir, el código está en un objeto y, por lo tanto, el objeto proporciona el tipo único de código.
Cuando se trabaja con funciones tradicionales, esta técnica no funciona bien porque un cambio de tipo no indica una función específica , sino que especifica solo la firma de muchas funciones posibles. Asi que:
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 al caso del funtor. 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. (No lo descartaría, ya que la optimización del compilador se ha avanzado bastante).
Una forma de decir que este código no hace exactamente 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á introduciendo. Para obtener una inscripción completa, necesitamos plantilla por valor, de modo 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 crea una instancia con una función específica ya disponible. Por lo tanto, esperamos que el código para do_op se parezca mucho a "return a + b". (Programadores Lisp, ¡deja de sonreir!)
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);
no podrá 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 al momento del compilador para saber cuál es nuestra op.
Entonces, si el segundo ejemplo realmente alinea totalmente nuestra operación, y el primero no, ¿de qué sirve la plantilla? ¿Qué está haciendo? La respuesta es: tipo 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 un buen C ++ pero ...) Lo que sucedió es que do_op se ha modelado en torno a las firmas de las distintas funciones, y cada instanciación separada escribirá un código de coerción de tipo diferente. Así que el código instanciado para do_op con fadd se ve algo así 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 coincidencia exacta en los argumentos de la función.
Los punteros de función se pueden pasar como parámetros de plantilla, y esto es parte de C ++ estándar . Sin embargo, en la plantilla se declaran y se utilizan como funciones en lugar de puntero a función. En la creación de instancias de plantilla, 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 de functor 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 de funtor como un 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 cercano a este aspecto uniforme con un argumento de plantilla es definir do_op
dos veces, una vez con un parámetro que no sea de tipo y una vez 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 que esto no se compilara, pero me funcionó con gcc-4.8 y Visual Studio 2013.
Sí, es válido.
En cuanto a hacerlo funcionar también con funtores, la solución habitual es algo como esto:
template <typename F>
void doOperation(F f)
{
int temp=0;
f(temp);
std::cout << "Result is " << temp << std::endl;
}
que ahora se puede llamar como:
doOperation(add2);
doOperation(add3());
El problema con esto es que si resulta complicado para el compilador add2
la llamada a add2
, ya que todo lo que sabe el compilador es que se está pasando a doOperation
un tipo de puntero de función void (*)(int &)
. (Pero add3
, al ser un functor, se puede insertar fácilmente. Aquí, el compilador sabe que se pasa un objeto de tipo add3
a la función, lo que significa que la función a llamar es add3::operator()
, y no solo algo desconocido puntero de función.)