4.9.18f Sobrecarga del operador de invocación de función
§1 Antecedentes:
La invocación de funciones ( 4.4.6)
en C++ tiene la siguiente sintaxis general:
expresión-postfija ( <lista-de-expresiones> );
§1.1 En su utilización normal, expresión-postfija es el nombre de una función, un puntero a función o
la referencia a una función. Por ejemplo:
float sum(int i, int j) {
float s = i + j;
cout << "La suma es: " << s << endl;
return s;
}
...
float (*fptr)(int, int) = sum; // definición de puntero-a-función
float (&fref)(int, int) = sum; // definición de referencia-a-función
int x = 2, y = 5;
sum(x*2, y);
// Ok. invocación
fptr(x*2, y);
// Ok. invocación
fref(x*2, y);
// Ok. invocación
§1.2 Cuando se utiliza con funciones-miembro (métodos), expresión-postfija es el nombre de un método,
una expresión de puntero-a-clase (utilizado para seleccionar un método), o de puntero-a-miembro. Por ejemplo:
class Vector { // una clase cualquiera
float x, y;
public: void getm(int i) { // función-miembro (método)
cout << "Vector: (" << x * i <<
", " << y * i << ") " << endl;
}
};
...
Vector v1;
// Objeto
Vector* vptr = &v1; //
definición de puntero-a-clase
void (Vector::* vmptr) (int) // definición de puntero-a-miembro
= &Vector::getm;
int x = 2;
v1.getm(x);
// Ok. invocación del método
vptr->getm(x);
// Ok. Ídem.
(v1.*vmptr)(x); // Ok. Ídem.
En este contexto nos referimos al paréntesis ( ) como operador de invocación de función
( 4.9.16),
aunque sabemos que tiene otros usos en el lenguaje: servir de signo de puntuación
(
3.2.6) y delimitador en
algunas expresiones. Por ejemplo, las expresiones con coma (
4.10.5).
§2 Sinopsis:
La gramática C++ permite definir una función-miembro no estática operator( ) cuya definición sea del tipo [1]:
valor-devuelto operator( )( <lista-de-argumentos> ) { /* definición */ } // §2a
En este caso, las instancias de clases en que se han definido estos métodos, presentan una curiosa peculiaridad sintáctica: que sus métodos operator() pueden ser invocados utilizando directamente el identificador del objeto como expresión-postfija es decir:
obj( <lista-de-argumentos> );
siendo obj una instancia de la clase Cl para la que se define la función operator( ). Por ejemplo:
class Cl {
public:
...
void operator()(int x) { /* definición */ }
...
};
...
Cl obj;
obj(5); // Ok!!
Cuando se utiliza esta notación, el compilador transforma la expresión anterior en una invocación a operator( ) en la forma canónica:
obj.operator()( <lista-de-argumentos> ); // §2b
Observe que se trata simplemente de la invocación de una función-miembro sobre el objeto obj, y que nada impide que sea
invocada directamente al modo tradicional con la sintaxis canónica §2b
. Es decir, utilizando explícitamente la sustitución realizada
por el compilador.
No confundir la expresión anterior (§2a
) con la utilización de
operator( ) como operador de conversión (
4.9.18k), donde se utiliza sin especificación del valor devuelto y sin que
pueda aceptar ningún tipo de parámetro:
operator( ){ /* valor devuelto */ } // §2c
Recordemos que operator( ) puede aparecer con dos significados en el interior de una clase:
class C {
valor-devuelto operator()(argumentos); // operador de invocación a función
operator() { /* ... */ }
// operador de conversión
...
};
§2.1 Lo ilustramos con un ejemplo:
#include <iostream>
using namespace std;
class Vector {
public:
float x, y;
void operator()()
{ // función-operador
cout << "Vector: (" << x << ", " << y << ") " << endl;
}
};
void main () { // =================
Vector v1 = {2, 3};
v1();
// Ok. invocación de v1.operator()
v1.operator()(); // Ok. invocación clásica
}
Salida
Vector: (2, 3)
Vector: (2, 3)
§3 El operador de invocación de función no es sobrecargable
Respecto a la "sobrecarga" del operador de invocación de función ( ), podemos decir algo análogo a lo indicado
para la sobrecarga del selector indirecto ->; (
4.9.18e): a pesar de que en la literatura sobre el tema, la descripción
de la función operator( ) se encuentra siempre en el capítulo dedicado a la sobrecarga de operadores, en realidad no se
trata de tal sobrecarga. Al menos no en un sentido homogéneo al empleado con el resto de opradores. Hemos visto que se trata de una
mera curiosidad sintáctica; una forma algo extraña de invocación de determinadas funciones-miembro (cuando estas funciones
responden a un nombre especial).
Al hilo de lo anterior, y dado que el identificador operator( ) es único, resulta evidente que si se definen varias de
estas funciones, se aplicará la congruencia estándar de argumentos
( 4.4.1a) para
resolver cualquier ambigüedad. Por ejemplo:
#include <iostream>
using namespace std;
class Vector {
public: float x, y;
void operator()() { // L.6 Versión-1
cout << "Vector: (" << x << ", " << y << ") " << endl;
}
void operator()(int i) { // L.9 Versión-2
cout << "Coordenadas: (" << x << ", " << y << ") " << endl;
}
};
void main () { // ============
Vector v1 = {2, 3};
v1(); // Ok. invoca versión-1 §b
v1(1); // Ok. invoca versión-2 §c
}
Salida:
Vector: (2, 3)
Coordenadas: (2, 3)
Comentario
Observe que en L.9, el argumento (int) de la segunda definición se ha utilizado exclusivamente para permitir al
compilador distinguir entre ambas. Esta técnica ya la hemos visto en la sobrecarga de los post-operadores incremento y decremento
( 4.9.18c).
§4 Objetos-función
Lo indicado hasta aquí podría parecer un mero capricho sintáctico del creador del
lenguaje; una forma particular de invocación
de ciertas funciones-miembro (de nombre especial), que presentan la singularidad de permitir utilizar objetos como si fuesen
funciones (caso de las expresiones §b y §c ), pero que no
tienen una justificación objetiva, ya que no resuelve un problema que no pueda ser resuelto con alguno de los recursos existentes
en el lenguaje.
Precisamente, en razón de que pueden ser utilizadas como funciones, las instancias de clases para las que se han definido funciones operator( ), reciben indistintamente el nombre de objetos-función, funciones-objeto [2] o functor, y algún autor ha definido a estas entidades como "datos ejecutables" [3].
En realidad, como ocurre con otros detalles de su diseño, este aparente capricho sintáctico encierra un mundo de sutilezas. Su
importancia y razón de ser estriban en que permite escribir código que realiza operaciones complejas a través de argumentos de
funciones ( 5.1.3a1). Precisamente la
Librería Estándar de Plantillas C++ (STL
5.1) o sus extensiones, como las librerías Boost, donde se encuentran algunos de los conceptos y algoritmos más sofisticados que haya construido hasta
el momento la ingeniería de software, utiliza con profusión este tipo de recursos.
Recuerde que los objetos (instancias de clases) pueden ser pasados como argumentos de funciones y que sus métodos pueden acceder a las propiedades de la clase, de forma que pasar un objeto-función como argumento, equivale a pasar a la función más información de la que supondría un escalar. Esta circunstancia tiene muchas aplicaciones. Por ejemplo, la nueva versión del Estándar permitirá crear en una aplicación un hilo ("thread") de ejecución mediante una expresión del tipo:
void work_to_do();
// L.1
std::thread newThread (work_to_do); // L.2
La función work_to_do() define el proceso que se ejecutará en el nuevo hilo representado por el objeto newThread de L.2. Sin embargo, el constructor de la clase std::trhead exige como argumento un puntero a función que no recibe argumentos y devuelve void. De forma que no es posible pasar en el constructor ninguna información adicional sobre detalles de la tarea a realizar y es en este punto, donde las características de C++ vienen al rescate, porque al igual que muchos otros algoritmos de la Librería Estándar C++, el argumento no tiene porqué ser necesariamente una función ordinaria; también puede ser un objeto-función, así que el diseño podría ser como sigue:
class Work_to_do {
public:
// miembros que representan particularidades del proceso
int a_, b_;
// miembro que representa el resultado del proceso
int& c_;
// constructor que permite fijar las características
Work_to_do (int a, int b, int& c) : a_(a), b_(b), c_(c) {}
// operador de invocación a función
void operator()() { c_ = a_ + b_; }
};
...
int r;
std::thread newThread (Work_to_do(1,2,r));
Observe que el argumento Work_to_do(x,y,z) es una llamada al constructor de la clase, que genera un objeto-función; que a su vez, es pasado al constructor de la clase std::trhead para construir el objeto newThread (que representa el nuevo hilo). Una vez concluida la tarea, el resultado lo obtenemos en r.
§4.1 Unión de argumentos
Observe que en el ejemplo anterior, el recurso ha consistido en empaquetar los argumentos involucrados en un objeto-función. Esta técnica, conocida como unión o empaquetado de argumentos ("argument binding") es ampliamente utilizada, aunque tal como la hemos presentado, tiene el inconveniente de que hay que preparar manualmente la clase adecuada. Sin embargo, si como suele ser frecuente [4] la situación se repite, es posible automatizarla utilizando una plantilla.
Supongamos que el proceso que deba ejecutar la hebra ("thread") pueda ser definida genéricamente mediante una función del tipo
void work_to_do (A a, B b, C& c);
En la que los objetos a y b representan los datos particulares
y c la variable en la que se obtiene el resultado. En estas
circunstancias, es posible definir una clase genérica (
4.12.2) tal como:
template <typename A, typename B, typename C> class bind {
public:
A a_;
B b_;
C& c_;
void (*pf)(A,B,C&);
bind (void (*p)(A,B,C&), A a, B b, C& c)
: pf(p), a_(a), b_(b), c_(c) {}
void operator()() { pf(a_, b_, c_); }
};
Suponiendo que tenemos definida la función que realiza el proceso en un caso concreto:
void process1 (int a, char b, float& c) {
/* proceso a realizar por la hebra
el resultado es situado en c */
...
}
Para lanzar una hebra que realizara esa tarea, solo tendríamos que incluir un par de líenas en nuestro código:
float f;
std:thread tread1 (bind (process1, 2, 'c', f));
[1] Recuerde que operator( ) es el identificador de una función-miembro no estática (el identificador incluye los paréntesis).
[2] Particularmente nos parece más acertado para el Español el término objeto-función, dado que en realidad se trata de instancias de clases (objetos). Otra cosa es que en inglés, el adjetivo preceda al nombre ("Function object").
[3] Jak Kirman "A Very Modest STL Tutorial"
www.cs.brown.edu
[4] Adelantamos aquí que este tipo de funciones (kallbacks) son ampliamente utilizadas por algunos algoritmos de la nueva versión del Estándar, por lo que este incluye también unas plantillas std::bind que facilitan en gran medida el trabajo de codificación en estos casos.