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á prometiendo 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 a través del valor de retorno.

Al principio, esto puede hacer suponer que el especificador const tiene poco significado. Puede ver la aparente falta de efecto 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 predefinidos, 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 predefinidos 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 valor izquierdo (es decir, no se le puede asignar nada ni se puede modificar de ningún otro modo).

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. Sólo el valor de retorno por valor no constante se puede usar como valor izquierdo.

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

La razón por la que const no tiene sentido cuando se usa para devolver por valor variables de tipos predefinidos es que el compilador impide automáticamente el uso de dichos tipos como valor izquierdo, ya que devuelve un valor, no una variable. Sólo cuando se devuelven objetos por valor de tipos definidos por el programador esta cuestión toma importancia.

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 Capítulo 11). En la práctica es lo mismo que tomar un puntero no constante, aunque la sintaxis es diferente. La razón por la que no compila en C++ es la creación de un objeto temporal.

Objetos temporales

A veces, durante la evaluación de una expresión, el compilador debe crear objetos temporales. 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 objetos temporales es que siempre son constantes. Como normalmente no manejará objetos temporales, hacer algo que cambie un objeto temporal es casi seguro un error porque no será capaz de usar esa información. Para evitar esto, el compilador crea todos los objetos temporales como 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 objeto temporal 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 objeto temporal se copiaría en f7() y no importaría lo que le ocurriese al X temporal.

Sin embargo, f7() toma su argumento por referencia, lo que significa que en este ejemplo toma la dirección del X temporal. Como f7() no toma su argumento por referencia constante, tiene permiso para modificar el objeto temporal. Pero el compilador sabe que el objeto temporal desaparecerá en cuanto se complete la evaluación de la expresión, y por eso cualquier modificación hecha en el objeto temporal se perderá. Al hacer que todos los objetos temporales sean constantes automáticamente, esta situación provoca un error de compilación, de modo que no caerá en lo que sería un error muy difícil de localizar.

Sin embargo, fíjese en que las expresiones siguientes son legales:

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 objeto temporal para albergar el valor de retorno. De modo que en ambas expresiones el objeto temporal se modifica y, tan pronto como termina la expresión, el objeto temporal 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.