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]


5.1.3a Funciones y predicados

§1 Funciones como argumento

Hemos indicado ( 4.4) que las funciones C/C++ son la agrupación de cierta "funcionalidad" (representada por un algoritmo) en una forma que permite reutilizarlo varias veces a lo largo del programa [1].  Cuando se desea que el algoritmo presente un cierto grado de "adaptabilidad" o "polimorfismo", adecuándose a diversas circunstancias o presentando diversos comportamientos, se recurre a utilizar argumentos. Estos argumentos permiten la parametrización del algoritmo, de forma que su comportamiento concreto depende de los valores pasados en cada caso.

Una forma de añadir aún más adaptabilidad a las funciones es permitir la utilización de punteros a otras funciones como argumentos. En estos casos, el usuario de la función puede modificar más finamente el comportamiento del algoritmo definiendo adecuadamente la función que utilizará como argumento (observe que esto representa sacar parte de la funcionalidad fuera de la función y ponerla en manos del usuario). Teniendo esto en mente, no es de extrañar que los algoritmos utilizados en la Librería Estándar utilicen muy frecuentemente funciones entre sus argumentos. En la terminología de la STL estas funciones son conocidas por generadores ("Generator" );  funciones unarias ("Unary functions" ) y funciones binarias ("Binary functions" ) según que no acepten argumentos o acepten uno o dos respectivamente.

Nota: también existen funciones ternarias, cuaternarias, Etc. Según acepten tres, cuatro, o más argumentos, pero en la STL no se presentan algoritmos que acepten funciones de más de dos argumentos. Las funciones utilizadas para este fin son del tipo f(); f(x) y f(x, y).  Todas las demás son refinamientos de estos.

Como curiosidad, señalar que en ocasiones, es necesario utilizar con algotitmos de la STL, funciones que no corresponden con los tipos f(); f(x) o f(x, y) esperados porque tienen mayor número de argumentos. Por ejemplo, una función de tipo f(x, y) con un algoritmo que espera una función unaria f(x). En estos casos, la propia STL ofrece recursos, como las plantillas bind1st, bind2nd, binder1st y binder2nd para empaquetar los argumentos sobrantes, de forma que se pueda soslayar el problema.


Uno de los muchos ejemplos que se podrían citar, es el algoritmo for_each(), que invoca una función pasada como argumento sobre cada elemento del contenedor dentro de un rango. Por ejemplo, el código que sigue aplica la función printElement() para producir una salida que describe cada elemento en una lista de valores enteros.

void printElement (int value) {
  cout << "La lista contiene " << value << endl;
}

main () {
  list<int> aList;
  ...
  for_each (aList.begin(), aList.end(), printElement);
}

Nota:  la función genérica for_each( 5.1.3b1) es la primera que aparece en la cabecera <algorithm> de la STL. Responde al siguiente prototipo:

template <class InputIterator, class Function>

    void for_each (InputIterator first, InputIterator last, Function f);

Vemos que no devuelve nada y recibe tres argumentos, dos iteradores de entrada ( 5.1.2a) y una función.


Las funciones pasadas como argumento pueden realizar operaciones más complejas. Por ejemplo, supongamos un contenedor cuyos miembros son una serie de números; un contenedor tipo vector con 100 enteros del 0 al 99:

vector<int> vInt;
for (int i=0; i < 100; i++) vInt.push_back(i);

Sería muy fácil construir otro contenedor vIm3, con los miembros del contenedor anterior que cumpliesen una condición. Por ejemplo, ser múltiplos de tres. Para ello construimos un contenedor vacío:

vector<int> vIm3;

A continuación declaramos un objeto-función m3 que realice la comprobación que necesitamos:

class M3 {
  public:
  operator() (int& n) {
    return !(n % 3 == 0 );
  }
};

Finalmente lo utilizamos como argumento de un algoritmo (consulte el manual de su compilador para una completa descripción del algoritmo remove_copy_if)

remove_copy_if (vInt.begin(),
                vInt.end(),
                back_inserter(vIm3),
                M3());

Observe que el cuarto argumento M3() es en realidad una instancia m de la clase M3 anterior, obtenida mediante una invocación a su constructor por defecto, de forma que la sentencia anterior equivale a:

M3 m = M3();

remove_copy_if (vInt.begin(),
                vInt.end(),
                back_inserter(vIm3),
                m);

§1.1 Generador

Un generador ("generator") es un tipo de objeto-función que puede ser invocado sin argumentos. Representan cuestiones tales como un estado local, o la realización de una operación de entrada/salida. Se acepta que la propia invocación de la función puede alterar el estado del objeto, por lo que generalmente proporcionan resultados distintos cada vez que son invocadas. Un generador sería por ejemplo un generador de números aleatorios, aunque hay muchas más aplicaciones.


§1.2 Función unaria

Las funciones unarias aceptadas como argumentos por los algoritmos de la STL, se utilizan generalmente para definir las operaciones que se realizarán sobre cada uno de los elementos de una secuencia. Cuando un algoritmo acepta una de estas funciones, se indica en el prototipo con el especificador UnaryOperation. Por ejemplo, una de las formas del algoritmo transform ( 5.1.3c3) responde al siguiente prototipo:

template <class InputIterator, class OutputIterator, class UnaryOperation>
    OutputIterator transform (InputIterator first,

                              InputIterator last,

                              OutputIterator result,

                              UnaryOperation op);

Como puede verse, el cuarto argumento op, es una función unaria.

§1.3 Función binaria

Cuando un algoritmo acepta una función binaria como argumento, se indica en su prototipo mediante el especificador BinaryOperation. Por ejemplo, el algoritmo transform() antes mencionado, dispone de una versión sobrecargada que acepta una de estas funciones como último argumento:

template <class InputIterator1, class InputIterator2,
         class OutputIterator, class BinaryOperation>
    OutputIterator transform (InputIterator1 first1,

                              InputIterator1 last1,

                              InputIterator2 first2,

                              OutputIterator result,

                              BinaryOperation binary_op);


Las funciones binarias utilizadas por los algoritmos de la STL suelen ser aplicadas a valores desde dos secuencias diferentes. Por ejemplo, supongamos que tenemos una lista de cadenas de caracteres y una lista de enteros, y queremos replicar cada elemento de la primera lista el número de veces indicado en la segunda. Esto puede realizarse muy fácilmente utilizando la función transform(), que utiliza como argumento una de estas funciones (ver una descripción de este algoritmo y un ejemplo en 5.1.3c3).

§2 Predicados

Un caso importante de la STL lo representan los algoritmos que reciben funciones que devuelven un bool ( 3.2.1b ) o un entero (que puede reducirse a un booleano [2]). Este tipo de funciones, o punteros-a-función, se denominan de forma genérica predicados.  Simplemente predicado ("predicate"), predicado binario ("binary predicate") o generador ("generator"), según que las funciones utilizadas como argumento reciban a su vez uno, dos o ningún argumento respectivamente.

A título de ejemplo se muestra una de estas funciones que recibe un entero y devuelve un booleano, señalando si el argumento representa un año bisiesto ("leap year"):

bool isLeapYear (unsigned int year) {

  if((year % 4 == 0 && year % 100 !=0) || year % 400 == 0)
     return true;
  return false;
}

Nota: el criterio para establecer si un año es o no bisiesto (Febrero tiene 29 días) es el siguiente: son bisiestos los años divisibles por 4 pero no por 100, excepto los que sean divisibles por 400 (que también lo son).


Cuando un algoritmo de la STL acepta un predicado, se indica en el prototipo de la función genérica utilizando alguno de los calificadores Predicate o BinaryPredicate . según que la función reciba uno o dos argumentos

Nota: el diseño de la STL ha procurado que los predicados sean definidos como métodos de clases; concretamente el método operator(), con lo que están representados por objetos-función ( 5.1.3a1).

§2.1 Predicate

Estos algoritmos aceptan uno o varios iteradores, alguno de ellos identificado con una expresión del tipo SomeIterator first. Ejemplo:

find_if(InputIterator first, InputIterator last, Predicate pred);


El calificador Predicate señala que el argumento correspondiente (pred), es un objeto-función en el que el método pred.operator() acepta como argumento un miembro del contenedor sobre el que opera el algoritmo; concretamente el señalado por el argumento first.  Estas consideraciones indican que el objeto-función pred debe funcionar correctamente en una sentencia del tipo:

if ( pred(*first) ) { ... };


Estos argumentos son generalmente utilizados para señalar una condición. Por esta razón, los algoritmos que aceptan un Predicate terminan su nombre con la desinencia _if.  Por ejemplo: find_if(); count_if(), etc. Para mostrar un ejemplo concreto, tomemos este último, que responde al siguiente prototipo: 

template<class InputIterator, class Predicate>
   InputIterator find_if(InputIterator first, InputIterator last, Predicate pred);


Esta función genérica recorre un rango [first, last) de una secuencia ( 5.1.2) y devuelve un iterador al primer miembro que satisface la condición señalada en el predicado. En caso contrario devuelve el límite superior del rango (last).

Suponiendo que tenemos una lista aList cuyos elementos son enteros que representan años, podemos utilizar la función isLeapYear() anterior como predicado para el algoritmo find_if() para que encuentre el primer año bisiesto de nuestra lista:

list<int>::iterator firstLeap = 
    find_if(aList.begin(), aList.end(), isLeapYear);


Observe que estos algoritmos, como find_if, aceptan como argumento una función, en realidad un puntero a función ( Nota-1); no el resultado de una invocacón a la función (que es diferente):

find_if(aList.begin(), aList.end(), isLeapYear(n));  // Error!!

§2.2 BinaryPredicate

El calificador BinaryPredicate se utiliza en algoritmos cuyo prototipo adopta una forma análoga a las señaladas a continuación, que corresponden a los algoritmos find_end() y search_n() respectivamente:

template <class ForwardIterator1, class ForwardIterator2,
          class BinaryPredicate>
    ForwardIterator1 find_end (ForwardIterator1 first1, ForwardIterator1 last1,
                               ForwardIterator2 first2, ForwardIterator2 last2,
                               BinaryPredicate pred);

 

template <class ForwardIterator, class Size, class T,

          class BinaryPredicate>
    ForwardIterator1 search_n (ForwardIterator first, ForwardIterator last,
                               Size count, const T& value,
                                 BinaryPredicate pred);


Estos algoritmos aceptan uno o varios iteradores, incluyendo alguno de las combinaciones siguientes:

SomeIterator first1, SomeIterator first2

SomeIterator first1, const T& value


En estos casos, el calificador BinaryPredicate señala que el argumento pred es un objeto-función en el que el que el método pred.operator() acepta dos argumentos que son sendos miembros del contenedor sobre el que opera el algoritmo; concretamente los señalados por los argumentos first1 y first2, o first1 y value.  Estas consideraciones indican que el objeto-función pred debe funcionar correctamente en alguna de las siguientes sentencias:

if ( pred(*first1, *first2) ) { ... };

if ( pred(*first1, value) ) { ... };


  Inicio.


[1]  Desde un punto de vista funcional (dejando de lado aspectos organizativos y de claridad del código), una función que solo sea invocada una vez a lo largo del programa no tiene absolutamente ningún sentido.

[2] Siguiendo la tradición C, un entero distinto de cero equivale a cierto (true), y falso en caso contrario.