8.3.2. Retorno por valor constante

Algo similar ocurre con los valores de retorno. Si dice que el valor de retorno de una función es constante:

const int g();

está diciendo que el valor de la variable original (en el ámbito de la función) no se modificará. Y de nuevo, como lo está devolviendo por valor, es la copia lo que se retorna, de modo que el valor original nunca se podrá modificar.

En principio, esto puede hacer suponer que el especificador const tiene poco significado. Puede ver la aparente falta de sentido de devolver constantes por valor en este ejemplo:

//: C08:Constval.cpp
// Returning consts by value
// has no meaning for built-in types

int f3() { return 1; }
const int f4() { return 1; }

int main() {
  const int j = f3(); // Works fine
  int k = f4(); // But this works fine too!
} ///:~

Listado 8.5. C08/Constval.cpp


Para los tipos básicos, no importa si el retorno es constante, así que debería evitar la confusión para el programador cliente y no utilizar const cuando se devuelven variables de tipos básicos por valor.

Devolver por valor como constante se vuelve importante cuando se trata con tipos definidos por el programador. Si una función devuelve un objeto por valor como constante, el valor de retorno de la función no puede ser un recipiente [62]

Por ejemplo:

//: C08:ConstReturnValues.cpp
// Constant return by value
// Result cannot be used as an lvalue

class X {
  int i;
public:
  X(int ii = 0);
  void modify();
};

X::X(int ii) { i = ii; }

void X::modify() { i++; }

X f5() {
  return X();
}

const X f6() {
  return X();
}

void f7(X& x) { // Pass by non-const reference
  x.modify();
}

int main() {
  f5() = X(1); // OK -- non-const return value
  f5().modify(); // OK
//!  f7(f5()); // Causes warning or error
// Causes compile-time errors:
//!  f6() = X(1);
//!  f6().modify();
//!  f7(f6());
} ///:~

Listado 8.6. C08/ConstReturnValues.cpp


f5() devuelve un objeto de clase X no constante, mientras que f6() devuelve un objeto de clase X pero constante. Solo el valor de retorno por valor no constante se puede usar como recipiente.

Por eso, es importante usar const cuando se devuelve un objeto por valor si quiere impedir que se use como recipiente.

La razón por la que const no tiene sentido cuando se usa para devolver por valor variables de tipos del lenguaje es que el compilador impide automáticamente el uso de dichos tipos como recipiente, ya que devuelve un valor, no una variable. Solo cuando se devuelven objetos por valor de tipos definidos por el programador esta funcionalidad toma sentido.

La función f7() toma como argumento una referencia no constante (la referencia es una forma adicional para manejar direcciones en C++ y se trata en el [FIXME:XREF:capitulo 11]). Es parecido a tomar un puntero no constante, aunque la sintaxis es diferente. La razón por la que no compila es por la creación de un temporario.

Temporarios

A veces, durante la evaluación de una expresión, el compilador debe crear objetos temporales (temporarios). Son objetos como cualquier otro: requieren espacio de almacenamiento y se deben construir y destruir. La diferencia es que nunca se ven, el compilador es el responsable de decidir si se necesitan y los detalles de su existencia. Una particularidad importante de los temporarios es que siempre son constantes. Como normalmente no manejará objetos temporarios, hacer algo que cambie un temporario es casi seguro un error porque no será capaz de usar esa información. Para evitar esto, el compilador crea todos los temporarios como objetos constantes, de modo que le avisará si intenta modificarlos.

En el ejemplo anterior, f5() devuelve un objeto no constante. Pero en la expresión:

f7(f5());

el compilador debe crear un temporario para albergar el valor de retorno de f5() para que pueda ser pasado a f7(). Esto funcionaría bien si f7() tomara su argumento por valor; entonces el temporario se copiaría en f7() y no importaría lo que se pase al temporario X.

Sin embargo, f7() toma su argumento por referencia, lo que significa que toma la dirección del temporario X. Como f7() no toma su argumento por referencia constante, tiene permiso para modificar el objeto temporario. Pero el compilador sabe que el temporario desaparecerá en cuanto se complete la evaluación de la expresión, y por eso cualquier modificación hecha en el temporario se perderá. Haciendo que los objetos temporarios sean constantes automáticamente, la situación causa un error de compilación de modo que evitará cometer un error muy difícil de localizar.

En cualquier caso, tenga presente que las expresiones siguientes son correctas:

f5() = X(1);
f5().modify();

Aunque son aceptables para el compilador, en realidad son problemáticas. f5() devuelve un objeto de clase X, y para que el compilador pueda satisfacer las expresiones anteriores debe crear un temporario para albergar el valor de retorno. De modo que en ambas expresiones el objeto temporario se modifica y tan pronto como la expresión es evaluada el temporario se elimina. Como resultado, las modificaciones se pierden, así que probablemente este código es erróneo, aunque el compilador no diga nada al respecto. Las expresiones como éstas son suficientemente simples como para detectar el problema, pero cuando las cosas son más complejas los errores son más difíciles de localizar.

La forma de preservar la constancia de los objetos se muestra más adelante en este capítulo.



[62] N. del T.: «recipiente» corresponde con el término lvalue que se refiere a una variable que puede ser modificada o a la que se le puede asignar un valor.