Disponible la nueva versión "donationware" 7.3 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;  // 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));

 

  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

[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.