Función pasada como argumento de plantilla

225

Estoy buscando las reglas que implican pasar 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 "función como argumento de plantilla" no conduce a mucho. Y las plantillas clásicas de C ++ La guía completa sorprendentemente tampoco lo discute (al menos no desde mi búsqueda).

Las preguntas que tengo son si se trata de C ++ válido (o solo una extensión ampliamente compatible).

Además, ¿hay alguna manera de permitir que un functor con la misma firma se use indistintamente 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 forma similar a la forma en que puede pasar un puntero o functor 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>();

¡Apuntaría a un enlace web o dos, o una página en el libro de Plantillas C ++!

SPWorley
fuente
¿Cuál es el beneficio de una función como argumento de plantilla? ¿No se usaría el tipo de retorno como el tipo de plantilla?
DaClown
Relacionado: una lambda sin capturas puede descomponerse en un puntero de función, y puede pasar eso como un parámetro de plantilla en C ++ 17. Clang lo compila bien, pero el gcc actual (8.2) tiene un error y lo rechaza incorrectamente por tener "ningún vínculo" incluso con -std=gnu++17. ¿Puedo usar el resultado de un operador de conversión lambda constexpr sin captura C ++ 17 como un argumento sin tipo de plantilla de puntero de función? .
Peter Cordes

Respuestas:

124

Si, es valido.

En cuanto a hacer que funcione también con los functors, 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());

Míralo en vivo

El problema con esto es que si hace que sea difícil para el compilador alinear la llamada add2, ya que todo lo que sabe el compilador es que void (*)(int &)se está pasando un tipo de puntero de función doOperation. (Pero add3, al ser un functor, se puede alinear fácilmente. Aquí, el compilador sabe que un objeto de tipo add3se 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 desconocido).

jalf
fuente
19
Ahora aquí hay una pregunta interesante. Cuando se pasa el nombre de una 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
1
Existe una ventaja al usar functores sobre los punteros de función. El functor se puede instanciar dentro de la clase y, por lo tanto, proporciona más opertunity al compilador para optimizaciones (como inlining). El compilador tendría dificultades para optimizar una llamada sobre un puntero de función.
Martin York
11
Cuando la función se utiliza en un parámetro de plantilla, se "desintegra" en un puntero a la función pasada. Es análogo a cómo las matrices decaen en punteros cuando se pasan como argumentos a los parámetros. Por supuesto, el valor del puntero se conoce en tiempo de compilación y debe apuntar a una función con enlace externo para que el compilador pueda usar esta información con fines de optimización.
CB Bailey
55
Avancemos unos años más tarde, la situación con el uso de funciones como argumentos de plantilla mejoró mucho en C ++ 11. Ya no está obligado a utilizar Javaismos como las clases functor, y puede utilizar, por ejemplo, funciones en línea estáticas como argumentos de plantilla directamente. Todavía está lejos de compararse con las macros Lisp de los años 70, pero C ++ 11 definitivamente tiene un buen progreso a lo largo de los años.
pfalcon
55
dado que c ++ 11, ¿no sería mejor tomar la función como rvalue reference ( template <typename F> void doOperation(F&& f) {/**/}), por lo que bind, por ejemplo, puede pasar una expresión de vinculación en lugar de vincularla?
user1810087
70

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

La forma "tradicional" de C ++ de crear una plantilla de código es usar un functor, es decir, el código está en un objeto y, por lo tanto, 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 de tipo no indica una función específica , sino que solo especifica 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 al caso de functor. En este ejemplo, do_op se 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. (Sin embargo, no lo descartaría, ya que la optimización del compilador ha avanzado bastante).

Una forma de saber que este código no hace 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á alineando. Para obtener una alineación completa, necesitamos una plantilla por valor, para 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 Lisp, ¡deja de sonreír!)

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á al 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 momento del compilador para saber cuál es nuestra operación.

Entonces, si el segundo ejemplo está realmente en línea con 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 bueno en C ++ pero ...) Lo que sucedió es que do_op ha sido creado en torno a las firmas de las diversas funciones, y cada instancia por separado escribirá un código de coerción de tipo diferente. Entonces, el código instanciado para do_op con fadd se parece a:

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.

Ben Supnik
fuente
2
Vea stackoverflow.com/questions/13674935/… para una pregunta de seguimiento en respuesta directa a la observación aquí que int c = do_op(4,5,func_ptr);"claramente no se está alineando".
Dan Nissenbaum
Vea aquí para ver un ejemplo de esto en línea: stackoverflow.com/questions/4860762/… Parece que los compiladores se están volviendo bastante inteligentes en estos días.
BigSandwich
15

Los punteros de función se pueden pasar como parámetros de plantilla, y esto es parte del estándar C ++ . Sin embargo, en la plantilla se declaran y 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 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 functor 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 cercano que puede llegar a esta apariencia uniforme con un argumento de plantilla es definir do_opdos veces, una con un parámetro que no sea de tipo y otra 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 funcionó para mí con gcc-4.8 y Visual Studio 2013.

Kietz
fuente
9

En su plantilla

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

El parámetro Tes un parámetro de plantilla sin 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, cuyas constantes de puntero de función son).

Si desea algo que funcione tanto con objetos de función como con parámetros de función, necesita una plantilla escrita. Sin embargo, cuando hace 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 desmarca 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 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, ya que lo particular operator()está completamente determinado por el tipo de objeto de función.

CB Bailey
fuente
1

La razón de su ejemplo funtor no funciona es que se necesita una instancia para invocar al operator().

Nikolai Fetissov
fuente
0

Editar: Pasar el operador como referencia no funciona. Por simplicidad, entiéndalo como un puntero de función. Simplemente 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;
AraK
fuente