Disponible la versión 6 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


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.

  Inicio.


[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