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.
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.