12.3.3. Argumentos y valores de retorno

Puede parecer un poco confuso inicialmente cuando lea los archivos OverloadingUnaryOperators.cpp, Integer.h y Byte.h y vea todas las maneras diferentes en que se pasan y devuelven los argumentos. Aunque usted pueda pasar y devolver argumentos de la forma que prefiera, las decisiones en estos ejemplos no se han realizado al azar. Siguen un patrón lógico, el mismo que querrá usar en la mayoría de sus decisiones.

  1. Como con cualquier argumento de función, si sólo necesita leer el argumento y no cambiarlo, lo usual es pasarlo como una referencia const. Normalmente operaciones aritméticas (como + y -, etc.) y booleanas no cambiarán sus argumentos, así que pasarlas como una referencia const es lo que veré mayoritariamente. Cuando la función es un método, esto se traduce en una método const. Sólo con los operadores de asignación (como +=) y operator=, que cambian el argumento de la parte derecha, no es el argumento derecho una constante, pero todavía se pasa en dirección porque será cambiado.

  2. El tipo de valor de retorno que debe seleccionar depende del significado esperado del operador. (Otra vez, puede hacer cualquier cosa que desee con los argumentos y con los valores de retorno). Si el efecto del operador es producir un nuevo valor, necesitará generar un nuevo objeto como el valor de retorno. Por ejemplo, Integer::operator+ debe producir un objeto Integer que es la suma de los operandos. Este objeto se devuelve por valor como una constante así que el resultado no se puede modificar como un «valor izquierdo».

  3. Todas los operadores de asignación modifican el valor izquierdo. Para permitir que el resultado de la asignación pueda ser usado en expresiones encadenadas, como a = b = c, se espera que devuelva una referencia al mismo valor izquierdo que acaba de ser modificado. Pero ¿debería ser esta referencia const o no const?. Aunque lea a = b = c de izquierda a derecha, el compilador la analiza de derecha a izquierda, así que no está obligado a devolver una referencia no const para soportar asignaciones encadenadas. Sin embargo, la gente a veces espera ser capaz de realizar una operación sobre el elemento de acaba de ser asignado, como (a = b).func(); para llamar a func de a después de asignarle b. De ese modo, el valor de retorno para todos los operadores de asignación debería ser una referencia no const para el valor izquierdo.

  4. Para los operadores lógicos, todo el mundo espera obtener en el peor de los casos un tipo int, y en el mejor un tipo bool. (Las librerías desarrolladas antes de que los compiladores de C++ soportaran el tipo incorporado bool usaban un tipo int o un typedef equivalente).

Los operadores de incremento y decremento presentan un dilema a causa de las versiones postfija y prefija. Ambas versiones cambian el objeto y por tanto no pueden tratar el objeto como un const. La versión prefija devuelve el valor del objeto después de cambiarlo, así que usted espera recuperar el objeto que fue cambiado. De este modo, con la versión prefija puede simplemente revolver *this como una referencia. Se supone que la versión postfija devolverá el valor antes de que sea cambiado, luego está forzado a crear un objeto separado para representar el valor y devolverlo. Así que con la versión postfija debe devolverlo por valor si quiere mantener el significado esperado. (Advierta que a veces encontrará operadores de incremento y decremento que devuelven un int o un bool para indicar, por ejemplo, que un objeto preparado para moverse a través de una lista está al final de ella). Ahora la pregunta es: ¿Debería éste devolverse como una referencia const o no const?. Si permite que el objeto sea modificado y alguien escribe (a++).func(), func operará en la propia a, pero con (++a).func(), funcopera en el objeto temporal devuelto por el operador postfijo operator++. Los objetos temporales son automáticamente const, así que esto podría ser rechazado por el compilador, pero en favor de la consistencia tendría más sentido hacerlos ambos const como hemos hecho aquí. O puede elegir hacer la versión prefija no const y la postfija const. Debido a la variedad de significados que puede darle a los operadores de incremento y decremento, deben considerarse en términos del caso individual.

Retorno por valor como constante

El retorno por valor como una constante puede parecer un poco sutil al principio, así que es digno de un poco más de explicación. Considere el operador binario operator+. Si lo ve en una expresión como f(a+b), el resultado de a+b se convierte en un objeto temporal que se usará en la llamada a f(). Debido a que es temporal, es automáticamente const, así que aunque, de forma explicita, haga el valor de retorno const o no, no tendrá efecto.

Sin embargo, también es posible mandar un mensaje al valor de retorno de a+b, mejor que simplemente pasarlo a la función. Por ejemplo, puede decir (a+b).g() en la que g() es algún método de Integer, en este caso. Haciendo el valor de retorno const, está indicando que sólo un método const puede ser llamado sobre ese valor de retorno. Esto es correcto desde el punto de vista del const, porque le evita almacenar información potencialmente importante en un objeto que probablemente será destruido.

Optimización del retorno

Advierta la manera que se usa cuando se crean nuevos objetos para ser devueltos por valor. En operator+, por ejemplo:

return Integer(left.i + right.i);

Esto puede parecer en principio como una «función de llamada a un constructor» pero no lo es. La sintaxis es la de un objeto temporal; la sentencia dice «crea un objeto Integer temporal y desvuélvelo». A causa de esto, puede pensar que el resultado es el mismo que crear un objeto local con nombre y devolverlo. Sin embargo, es algo diferente. Si en su lugar escribiera:

Integer tmp(left.i + right.i);
    return tmp;

sucederían tres cosas. La primera, se crea el objeto tmp incluyendo la llamada a su constructor. La segunda, el constructor de copia duplica tmp en la localización del valor de retorno externo. La tercera, se llama al destructor para tmp cuando sale del ámbito.

En contraste, la aproximación de «devolver un objeto temporal» funciona de manera bastante diferente. Cuando el compilador ve eso, sabe que no tiene otra razón para crearlo mas que para devolverlo. El compilador aprovecha la ventaja que ofrece para construir el objeto directamente en la localización del valor de retorno externo a la función. Esto necesita de una sola y ordinaria llamada al constructor (la llamada al constructor de copia no es necesaria) y no hay llamadas al destructor porque nunca se crea un objeto local. De esta manera, no requiere nada más que el conocimiento del programador, y es significativamente mas eficiente. Esto a menudo se llama optimización del valor de retorno.