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; // Puntero-a-función
float (&fref)(int, int) = sum; // 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; // Puntero-a-clase
void (Vector::* vmptr) (int) // 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 {
...
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 se ha 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), 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.
[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