7: Sobrecarga de funciones y argumentos por defecto

Tabla de contenidos

7.1. Más decoración de nombres
7.2. Ejemplo de sobrecarga
7.3. Uniones
7.4. Argumentos por defecto
7.5. Elección entre sobrecarga y argumentos por defecto
7.6. Resumen
7.7. Ejercicios

Una de las características más importantes en cualquier lenguaje de programación es la utilización adecuada de los nombres.

Cuando crea un objeto (una variable) le está asignando un nombre a una región de memoria. Una función es un nombre para una acción. El hecho de poner nombres adecuados a la hora de describir un sistema hace que un programa sea más fácil de entender y modificar. Es muy parecido a la prosa escrita, el objetivo es comunicarse con los lectores.

Cuando se trata de representar sutilezas del lenguaje humano en un lenguaje de programación aparecen los problemas. A menudo, la misma palabra expresa diversos significados dependiendo del contexto. Una palabra tiene múltiples significados, es decir, está sobrecargada (polisemia). Esto es muy útil, especialmente cuando las diferencias son obvias. Puede decir «lave la camiseta, lave el coche.» Sería estúpido forzar la expresión anterior para convertirla en «lavar_camiseta la camiseta, lavar_coche el coche» pues el oyente no tiene que hacer ninguna distinción sobre la acción realizada. Los lenguajes humanos son muy redundantes, así que incluso si pierde algunas palabras, todavía puede determinar el significado. Los identificadores únicos no son necesarios, pues se puede deducir el significado a partir del contexto.

Sin embargo, la mayoría de los lenguajes de programación requieren que se utilice un identificador único para cada función. Si tiene tres tipos diferentes de datos que desee imprimir en la salida: int, char y float, generalmente tiene que crear tres funciones diferentes, como por ejemplo print_int(), print_char() y print_float(). Esto constituye un trabajo extra tanto para el programador, al escribir el programa, como para el lector que trate de entenderlo.

En C++ hay otro factor que fuerza la sobrecarga de los nombres de función: el constructor. Como el nombre del constructor está predeterminado por el nombre de la clase, podría parecer que sólo puede haber un constructor. Pero, ¿qué ocurre si desea crear un objeto de diferentes maneras? Por ejemplo, suponga que escribe una clase que puede inicializarse de una manera estándar o leyendo información de un fichero. Necesita dos constructores, uno que no tiene argumentos (el constructor por defecto) y otro que tiene un argumento de tipo string, que es el nombre del fichero que inicializa el objeto. Ambos son constructores, así pues deben tener el mismo nombre: el nombre de la clase. Así, la sobrecarga de funciones es esencial para permitir el mismo nombre de función (el constructor en este caso) se utilice con diferentes argumentos.

Aunque la sobrecarga de funciones es algo imprescindible para los constructores, es también de utilidad general para cualquier función, incluso aquellas que no son métodos. Además, la sobrecarga de funciones significa que si tiene dos librerías que contienen funciones con el mismo nombre, no entrarán en conflicto siempre y cuando las listas de argumentos sean diferentes. A lo largo del capítulo se mostrarán todos los detalles.

El tema de este capítulo es la elección adecuada de los nombres de la funciones. La sobrecarga de funciones permite utilizar el mismo nombre para funciones diferentes, pero hay otra forma más adecuada de llamar a una función. ¿Qué ocurriría si le gustara llamar a la misma función de formas diferentes? Cuando las funciones tienen una larga lista de argumentos, puede resultar tediosa la escritura (y confusa la lectura) de las llamadas a la función cuando la mayoría de los argumentos son lo mismos para todas las llamadas. Una característica de C++ comúnmente utilizada se llama argumento por defecto. Un argumento por defecto es aquel que el compilador inserta en caso de que no se especifique cuando se llama a la función. Así, las llamadas f("hello"), f("hi", 1) y f("howdy", 2, 'c') pueden ser llamadas a la misma función. También podrían ser llamadas a tres funciones sobrecargadas, pero cuando las listas de argumentos son tan similares, querrá que tengan un comportamiento similar, que le lleva a tener una única función.

La sobrecarga de funciones y los argumentos por defecto no son muy complicados. En el momento en que termine este capítulo, sabrá cuándo utilizarlos y entenderá los mecanismos internos que el compilador utiliza en tiempo de compilación y enlace.

7.1. Más decoración de nombres

En el Capítulo 4 se presentó el concepto de decoración de nombres. En el código:

void f();
class X { void f(); };

La función f() dentro del ámbito de la clase X no entra en conflicto con la versión global de f(). El compilador resuelve los ámbitos generando diferentes nombres internos tanto para la versión global de f() como para X::f(). En el Capítulo 4 se sugirió que los nombres son simplemente el nombre de la clase junto con el nombre de la función. Un ejemplo podría ser que el compilador utilizara como nombres _f y _X_f. Sin embargo ahora se ve que la decoración del nombre de la función involucra algo más que el nombre de la clase.

He aquí el porqué. Suponga que quiere sobrecargar dos funciones

void print(char);
void print(float);

No importa si son globales o están dentro de una clase. El compilador no puede generar identificadores internos únicos si sólo utiliza el ámbito de las funciones. Terminaría con _print en ambos casos. La idea de una función sobrecargada es que se utilice el mismo nombre de función, pero diferente lista de argumentos. Así pues, para que la sobrecarga funcione el compilador ha de decorar el nombre de la función con los nombres de los tipos de los argumentos. Las funciones planteadas más arriba, definidas como globales, producen nombres internos que podrían parecerse a algo así como _print_char y _print_float. Nótese que como no hay ningún estándar de decoración, podrá obtener resultados diferentes de un compilador a otro. (Puede ver lo que saldría diciéndole al compilador que genere código fuente en ensamblador). Esto, por supuesto, causa problemas si desea comprar unas librerías compiladas por un compilador y enlazador particulares, aunque si la decoración de nombres fuera estándar, habría otros obstáculos debido a las diferencias de generación de código máquina entre compiladores.

Esto es todo lo que hay para la sobrecarga de funciones: puede utilizar el mismo nombre de función siempre y cuando la lista de argumentos sea diferente. El compilador utiliza el nombre, el ámbito y la lista de argumentos para generar un nombre interno que el enlazador pueda utilizar.

7.1.1. Sobrecarga en el valor de retorno

Es muy común la pregunta «¿Por qué solamente el ámbito y la lista de argumentos? ¿Por qué no también el valor de retorno?». A primera vista parece que tendría sentido utilizar también el valor de retorno para la decoración del nombre interno. De esta manera, también podría sobrecargar con los valores de retorno:

void f();
int f();

Esto funciona bien cuando el compilador puede determinar sin ambigüedades a qué tipo de valor de retorno se refiere, como en int x = f();. No obstante, en C se puede llamar a una función y hacer caso omiso del valor de retorno (esto es, puede querer llamar a la función debido a sus efectos laterales). ¿Cómo puede el compilador distinguir a qué función se refiere en este caso? Peor es la dificultad que tiene el lector del código fuente para dilucidar a qué función se refiere. La sobrecarga mediante el valor de retorno solamente es demasiado sutil, por lo que C++ no lo permite.

7.1.2. Enlace con FIXME:tipos seguros

Existe un beneficio añadido a la decoración de nombres. En C hay un problema particularmente fastidioso cuando un programador cliente declara mal una función o, aún peor, se llama a una función sin haber sido previamente declarada, y el compilador infiere la declaración de la función mediante la forma en que se llama. Algunas veces la declaración de la función es correcta, pero cuando no lo es, suele resultar en un fallo difícil de encontrar.

A causa de que en C++ se deben declarar todas las funciones antes de llamarlas, las probabilidades de que ocurra lo anteriormente expuesto se reducen drásticamente. El compilador de C++ rechaza declarar una función automáticamente, así que es probable que tenga que incluir la cabecera apropiada. Sin embargo, si por alguna razón se las apaña para declarar mal una función, o declararla a mano o incluir una cabecera incorrecta (quizá una que sea antigua), la decoración de nombres proporciona una seguridad que a menudo se denomina como enlace con tipos seguros.

Considere el siguiente escenario. En un fichero está la definición de una función:

//: C07:Def.cpp {O}
// Function definition
void f(int) {}
///:~

Listado 7.1. C07/Def.cpp


En el segundo fichero, la función está mal declarada y en main se le llama:

//: C07:Use.cpp
//{L} Def
// Function misdeclaration
void f(char);

int main() {
//!  f(1); // Causes a linker error
} ///:~

Listado 7.2. C07/Use.cpp


Incluso aunque pueda ver que la función es realmente f(int), el compilador no lo sabe porque se le dijo, a través de una declaración explícita, que la función es f(char). Así pues, la compilación tiene éxito. En C, el enlazador podría tener también éxito, pero no en C++. Como el compilador decora los nombres, la definición se convierte en algo así como f_int, mientras que se trata de utilizar f_char. Cuando el enlazador intenta resolver la referencia a f_char, sólo puede encontrar f_int, y da un mensaje de error. Éste es el enlace de tipos seguro. Aunque el problema no ocurre muy a menudo, cuando ocurre puede ser increíblemente difícil de encontrar, especialmente en proyectos grandes. Éste método puede utilizarse para encontrar un error en C simplemente intentando compilarlo en C++.