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.4.7  Valor devuelto

§1  Sinopsis

Una función puede devolver cualquier tipo de valor a la función que la invoca (a excepción de una matriz o una función); incluso main puede devolver un valor al ámbito en que se está ejecutando el programa (entorno del sistema operativo), pero téngase en cuenta que aunque una función devuelva un valor, es potestad de la función invocante recibirlo o ignorarlo.

Un caso especial es cuando una función no devuelve nada [4]. Esto se señala en su definición y en su prototipo con la indicación void. Ejemplo:

void somefunc ( ...) { ...; }

En otros lenguajes las funciones que no devuelven nada reciben el nombre de procedimientos ("Procedures"). Así pues, los procedimientos serían un caso particular de las funciones C++.

Nota: como regla de buena programación C++, una función debe devolver siempre un valor al entorno de procedencia, aunque solo sea una indicación de que ha terminado sin problema o con error.

Como se ha señalado, en todos los demás casos la función devuelve un valor de cualquier tipo, excepto una matriz o una función.

tipoX somefunc ( ...) {
  ...
  return <expresion>;  // el resultado de <expresion> es promovido a tipoX
}

Nota: lo anterior es una verdad a medias (en C++ casi todo tiene alguna excepción o caso especial). Existe un tipo de funciones que no devuelve nada, ni siguiera void, son los constructores y destructores ( 4.11.2d). Por otra parte, los compiladores Borland C++ y MS Visual C++ disponen de mecanismos para avisar que una función no tiene retorno ( 4.4.1b).


§1.1    Recuerde que el operador de referencia no puede ser aplicado al valor devuelto por una función ( 4.9.11). Ejemplo:

x = &func(x);     // Error!!

La razón (ya señalada al hablar de los punteros 4.2), está en que no es posible obtener la dirección de una variable de registro, y el valor devuelto es de este tipo. Sin embargo, lo anterior no es óbice para que una función pueda devolver una referencia .


§1.2  Salvo cuando devuelve una referencia, el valor devuelto por una función es un Rvalue, que no puede ser utilizado como Lvalue.  Por ejemplo:

int func();
...
x = func();      // Ok.
func() = x;      // Error!

Sin embargo:

int& func(int&);
...
x = func(y);      // Ok.
func(y) = x;      // Ok.

§2  La sentencia return

Esta instrucción tiene un doble uso: definir el punto en que se devuelve el control de ejecución al punto de llamada y en su caso, devolver un valor.

§2.1  Sintaxis

return [<valor-devuelto>] ;

<valor-devuelto> es opcional; puede ser cualquier expresión que pueda reducirse a un valor del tipo a devolver por la función. Una función puede tener más de un punto de retorno (situación que debe ser evitada en la medida de lo posible), pero debe devolver el mismo tipo de dato en todos ellos [2].

Es ilegal incluir una declaración en la expresión <valor-devuelto>:

return int x = 10;        // Error!!

En muchos casos el valor devuelto es a su vez el resultado de otra invocación a función:

int somefunc() {
   ...
   return foo();
}

En este caso, el valor devuelto por foo es promovido a int antes de ser devuelto a su vez por somefunc.  En caso de que la conversión no fuese posible, se generaría un error de compilación.


§2.2  Si le ejecución de una función llega al cierre del bloque principal  } (externo) sin haber encontrado un return, el control vuelve a la función que la invocó, es decir: la sentencia return no es estrictamente necesaria en ninguna función, a menos que se desee devolver un valor, o devolver el control a la función invocante antes del final del cuerpo que se ejecuta.  Si la función invocante espera un valor y la función llamada llega a su fin sin haber encontrado un return adecuado, el valor recibido es basura.

Nota: existe una excepción, la función main, en la que si se alcanza el final sin haber encontrado un return, el compilador proporciona automáticamente un return int adecuado, ya que por definición, esta función devuelve int ( 4.4.4)

En el ejemplo siguiente se utiliza la sentencia return solo para devolver el control antes de alcanzar el final.

void valor (int x) {
  if (x == 0) return ;
  cout << "Valor = " << x << endl;
  return;        // este return es innecesario
}


§2.3  Observe que cuando la función no devuelve ningún valor (formalmente decimos que devuelve void), la instrucción de retorno es simplemente return, sin añadirle ningún valor:

void func (...) {
  ...
  return ;          // correcto
  return void;      // Error!
  return (void)0;   // correcto (se trata de un modelado de tipo)
  return void(0);   // correcto (variación sintáctica del anterior)
}


§2.4    Aunque coloquialmente hemos dicho que la función que devuelve void "no devuelve nada", el resultado de una invocación a este tipo de función es equivalente a "no-valor", por lo que puede ser utilizada como <valor-devuelto> en la expresión return de este tipo de funciones (que deban devolver void), es decir:

void func1 (int * ptr);
...
void func2 (int * ptr) {
   ...
   return func1(ptr);     // Ok. correcto
}


§2.5  Devolver un valor depende no solo de que se incluya la expresión return (<expresión>) en el cuerpo de la función;  también que haya sido previsto tal extremo en el prototipo y en la definición de la función. Recuérdese que en ausencia de otra especificación, toda función devuelve int, en consecuencia, las dos definiciones que siguen son equivalentes [3]:

int sum (int x, int y) { return x+y; }
sum (int x, int y) { return x+y; }


§2.6
  El valor devuelto, señalado por return <expresión>, es convertido (promovido) al tipo señalado en la definición (y en el prototipo) de la función antes que se produzca el retorno. Como ejemplo consideremos el siguiente caso:

func (double  dob) { return  dob+3; }

La función debe devolver un int (valor por defecto), pero devuelve un double. Como consecuencia, debe realizarse una conversión de tipo con pérdida de información, por lo que el compilador puede mostrar un mensaje de aviso. Para evitarlo, puede forzarse explícitamente una conversión ("casting") de tipo ( 4.9.9), con lo que el compilador no señalará ninguna advertencia.

func (double  doble) { return (int) doble+3; }

§3  Devolver por referencia

Cuando el tipo de valor devuelto por la función es una referencia ( 4.2.3) se dice que la función devuelve "por referencia". En estos casos debe avisarse de esta circunstancia al compilador mediante la utilización del declarador de referencia & después del indicador del tipo de valor devuelto. La sintaxis es la siguiente:

<tipo-devuelto>& nombre-funcion (<tipo> <parametro>, ... )

Ejemplo:

int & funcion (int x) {
   ....
   return z;
}


Observe que no es necesario poner return &z; basta la indicación relativa al tipo de valor devuelto (int &).

En los ejemplos y explicaciones que siguen aprenderá que las devoluciones por referencia pueden ser muy problemáticas, y que los valores devueltos son del tipo correspondiente, lo que permite utilizarlos como tales. Por ejemplo:

int & func(int x) {
  ...
  return y;
}
...
int& z = func(5);    // Ok el valor devuelto es referencia-a-int

Recuerde también lo indicado en §1.1 relativo a que no es posible utilizar el operador de referencia con el valor devuelto por la función. Es decir, en el caso anterior no está permitida una expresión como [7]:

z = & func(5);      // Error !!


§3.1  Un ejemplo:

int& max (int a, int b) {
  if (a >= b) return a;
  return b;
}

La definición anterior tiene un serio problema: que en cualquier caso, la función devuelve una referencia a una variable local, y es sabido (4.4.6b) que cuando la función termina, sus variables locales son sacadas de la pila y destruidas, sin embargo, supuestamente la función invocante todavía podría estar utilizándolas [6]. Por esta razón, cuando se intenta una definición de este tipo, el compilador devuelve un mensaje de error: Attempting to return a reference to local variable ....


§3.2
  Está permitido devolver una referencia a una constante local a la función [1], aunque el resultado no está garantizado y depende del compilador; algo como el ejemplo que sigue funciona en BC++ (por supuesto, no es equivalente al ejemplo §3.1 anterior):

const int& max (int a, int b) {
  const int kte = 100;
  if (a >= b) a = kte;
  else b = kte;
  return kte;
}


§3.3  Es totalmente correcto devolver una referencia a una variable estática, aunque sea local a la función, ya que permanecerá aún cuando la función sea terminada [5]. Por ejemplo:

int& max (int a, int b) {
  static int mx = 100
  if (a >= b) mx = a;
  else mx = b;
  return mx;
}


§3.4
  Aunque puedan ser admitidas con determinados compiladores y en determinadas circunstancias, las situaciones anteriores pueden ser calificadas como de "verdadera chapuza". Utilizar referencias devueltas por funciones es siempre problemático si el origen de la referencia está en el interior de la función invocada. Un caso distinto es cuando el origen está fuera de la función (la referencia devuelta pasó a su vez a la función por referencia §3.5 ) o es un valor externo conocido por la función §3.8 .

Incluso situaciones aparentemente correctas, como §3.5 , pueden resultar incontroladas en determinadas circunstancias. Por ejemplo, si existen varias invocaciones en la misma sentencia:

if ( max(a, b) > 5 && max(c, d) > 10) { /* ... */ }   // §3.4a

o si la función es invocada recursivamente:

int& max (int a, int b) {
  static int mx = 100
  if (a >= b) mx = a;
  else mx = max(a, 10*b);
  return mx;
}

En estas condiciones es preferible devolver los objetos "por valor", de forma que se reciba una copia del valor devuelto. Cuando esto no es posible, se puede recurrir a crear el objeto a devolver en la zona de almacenamiento persistente (montón ( 1.3.2), aunque es necesario recordar su destrucción posterior. En cualquier caso estas situaciones son propensas a errores, tienen muy difícil solución y deben evitarse.

§3.5  Cuando se devuelve una referencia, lo más frecuente es que sea justamente uno de los argumentos, que a su vez ha pasado por referencia ( 4.4.5), con lo que no habría problema, pues el valor devuelto referencia a un objeto fuera de la función. El ejemplo §3.1 se compilaría sin problemas añadiendo esta pequeña modificación que afecta a la definición de los parámetros:

int& max (int& a, int& b) {
  if (a >= b) return a;
  return b;
}


§3.6
  En este contexto, una llamada a la función en la forma:

max(x, y);

proporciona una referencia (alias) al mayor de los dos valores que pasan como argumento, en consecuencia es posible la utilización siguiente:

#include <iostream.h>
int& max (int& a, int& b) {if (a >= b) return a; return b; }

int main () {             // ==================
  int x =12, y = 22;
  cout << "Máximo: " << max(x, y) << endl;
  cout << "Valor inicial: " << y << endl;
  int& mx = max(x, y);    // M.4: Ok asignación del mismo tipo
  mx = 30;                // M.5:
  cout << "Valor final: " << y << endl;
}

Salida:

Máximo: 22
Valor inicial: 22
Valor final: 30

Comentario:

Como era de esperar la primera salida nos muestra el mayor valor entre x e y.

En M.4 la asignación es correcta ya que mx es tipo referencia-a-int, igual que el valor devuelto por la función; a partir de este momento  mx es sinónimo de la variable y (de mayor valor). Por su parte la asignación en M.5 equivale a  y = 30; lo que queda reflejado en la tercera salida.

§3.7  El ejemplo se podría sofisticar un poco más combinando M.4 y M.5.9 en una sola sentencia:

#include <iostream.h>
int& max (int& a, int& b) { return a >= b? a: b; }

int main () {              // ==================
  int x =12, y = 22;
  cout << "Máximo: " << max(x, y) << endl;
  cout << "Valor inicial: " << y << endl;
  max(x, y) = 30;          // M.4bis: Ok asignación del mismo tipo
  cout << "Valor final: " << y << endl;
}

La salida es idéntica al caso anterior.

La asignación en M.4bis, con la función a la izquierda (como un Lvalue), puede parecer un poco sorpresiva, pero perfectamente lógica si sustituimos mentalmente max(z, k) por una referencia a la mayor de las dos variables x e y.

  Esta capacidad de las referencias, comportarse como un Rvalue cuando están a la derecha de una asignación (L.8 ), y como un Lvalue cuando están a la izquierda (M.4bis), incluso cuando el valor es el resultado de una invocación a función, es exclusiva y única de las referencias, y la razón última de su introducción en el lenguaje, ya que este comportamiento no puede ser simulado mediante punteros y es requerido en circunstancias muy especiales.

§3.8  Otra circunstancia en que pueden utilizarse referencias devueltas por funciones es cuando el valor devuelto es conocido por la función.

#include <iostream.h>
class Vector {
  int x, y;              // private por defecto
  public:
  void setX(int x) { this->x = x; }
  int getX() { return x;}
  int& Y() { return y; }
  Vector() { x = 0; y = 0; }  // constructor por defecto
};

void main() {            // =============
  Vector v;
  int y = v.Y();         // M.2
  cout << "Valores iniciales\n"
       << "x = " << v.getX() << "\n"
       << "y = " << y << "\n";
// modificar miembros
  v.setX(10);
  v.Y() = 20;            // M.5
  cout << "Valores modificados\n"
       << "x = " << v.getX() << "\n"
       << "y = " << v.Y() << "\n";
}

Salida:

Valores iniciales
x = 0
y = 0
Valores modificados
x = 10
y = 20

Comentario

El ejemplo muestra dos procedimientos para manejar las propiedades privadas x e y de la clase. El primero utiliza dos métodos públicos, getX y setX, para manejar el componente x. En el segundo se utiliza una sola función Y, capaz de obtener el valor de la variable y, y al mismo tiempo modificarla.

Puede verse como el valor devuelto por esta función, una referencia a la propiedad y, puede ser utilizado indistintamente como Rvalue de una asignación (M.2) y como Lvalue (M.5). Observe que la referencia devuelta por Y es una propiedad de la clase que no pertenece al cuerpo de la función ni le ha sido pasada por referencia. En realidad se aprovecha la circunstancia de que las propiedades de la clase son accesible desde sus métodos ( 4.11.2e).

  Tema relacionado:
  • Funciones sin retorno ( 4.4.1b).

  Inicio.


[1]  Por razones de optimización, el tratamiento que da el compilador a las constantes que pueden ser resueltas en tiempo de compilación (desde el comienzo del programa) es muy especial.

[2]  En contra de lo que ocurre con otros lenguajes, en C++ no es posible que una función devuelva dos tipos distintos en puntos distintos. Otra cosa es que, dentro de ciertos límites, la función invocante pueda modificar el tipo del valor devuelto mediante un modelado ( 4.9.9).

[3]  Hemos repetido en varias ocasiones que esta asunción C++, es resultado de una larga tradición de C (suponer int en ausencia de un especificador de tipo), pero que puede no ser cierta en determinados compiladores, y que debe ser considerada como una práctica a extinguir ("deprecated").

[4]  "No devuelve nada" es en este caso una frase coloquial; hablando con propiedad deberíamos decir "Devuelve no valor", y en C++ "no valor" es distinto de "nada"  >:-)  2.2.1

[5]  Recordemos que las variables estáticas tienen su sitio de almacenamiento fuera de la pila ( 1.3.2).

[6]  Este tipo de argucia (mantener visible una referencia al interior de una función -o bloque de código- ya utilizado) sí está permitida en otros lenguajes (no en C/C++), y sirve para que el compilador no destruya la función o bloque de código, que se mantiene visible mientras que exista la referencia en cuestión.

[7]  Sin embargo esta sentencia, que es errónea en C++, es justamente la que debe utilizarse en otros lenguajes (por ejemplo php) cuando el valor devuelto es "por referencia".