Uno de los usos más importantes de inline dentro de clases son las funciones de acceso. Se trata de pequeñas funciones que le permiten leer o cambiar parte del estado de un objeto, es decir, una o varias variables internas. La razón por la que inline es tan importante para las funciones de acceso se puede ver en el siguiente ejemplo:
//: C09:Access.cpp // Inline access functions class Access { int i; public: int read() const { return i; } void set(int ii) { i = ii; } }; int main() { Access A; A.set(100); int x = A.read(); } ///:~
Listado 9.3. C09/Access.cpp
Aquí, el usuario de la clase nunca tiene contacto directo con
las variables de estado internas a la clase, y pueden mantenerse
como privadas, bajo el control del diseñador de la clase. Todo
el acceso a los atributos se puede controlar a través de los
métodos de la interfaz. Además, el acceso es notablemente
eficiente. Considere read()
, por
ejemplo. Sin inline, el código generado para la llamada a
read()
podría incluir colocarla en la pila
y ejecutar la llamada CALL de ensamblador. En la mayoría de las
arquitecturas, el tamaño de ese código sería mayor que el código
creado para la variante inline, y el tiempo de ejecución sería
mayor con toda certeza.
Sin las funciones inline, un diseñador de clases preocupado
por la eficiencia estaría tentado de hacer que
i
fuese un atributo público, eliminado la
sobrecarga y permitiendo al usuario acceder directamente a
i
. Desde el punto de vista del diseñador,
eso resulta desastroso, i
sería parte de la
interfaz pública, lo cual significa que el diseñador de la
clase no podrá cambiarlo en el futuro. Tendrá que cargar con
un entero llamado i
. Esto es un problema
porque después puede que considere mejor usar un
float
en lugar de un int
para
representar el estado, pero como i
es parte
de la interfaz pública, no podrá cambiarlo. O puede que
necesite realizar algún cálculo adicional como parte de la
lectura o escritura de i
, que no podrá
hacer si es público. Si, por el contrario, siempre usa
métodos para leer y cambiar la información de estado del
objeto, podrá modificar la representación subyacente del
objeto hasta estar totalmente convencido.
Además, el uso de métodos para controlar atributos le permite añadir código al método para detectar cuando cambia el valor, algo que puede ser muy útil durante la depuración. Si un atributo es público, cualquiera puede cambiarlo en cualquier momento sin que el programador lo sepa.
Hay gente que divide el concepto de funciones de acceso en dos: accesores (para leer la información de estado de un objeto) y mutadores (para cambiar el estado de un objeto). Además, se puede utilizar la sobrecarga de funciones para tener métodos accesores y mutadores con el mismo nombre; el modo en que se invoque el método determina si se lee o modifica la información de estado. Así,
//: C09:Rectangle.cpp // Accessors & mutators class Rectangle { int wide, high; public: Rectangle(int w = 0, int h = 0) : wide(w), high(h) {} int width() const { return wide; } // Read void width(int w) { wide = w; } // Set int height() const { return high; } // Read void height(int h) { high = h; } // Set }; int main() { Rectangle r(19, 47); // Change width & height: r.height(2 * r.width()); r.width(2 * r.height()); } ///:~
Listado 9.4. C09/Rectangle.cpp
El constructor usa la lista de inicialización (brevemente
introducida en el capítulo 8 y ampliamente cubierta en el
capitulo 14) para asignar valores a wide
y high
(usando el formato de
pseudo-constructor para los tipos de datos básicos).
No puede definir métodos que tengan el mismo nombre que los atributos, de modo que puede que se sienta tentado de distinguirlos con un guión bajo al final. Sin embargo, los identificadores con guiones bajos finales están reservados y el programador no debería usarlos.
En su lugar, debería usar «set» y «get» para indicar que los métodos son accesores y mutadores.
//: C09:Rectangle2.cpp // Accessors & mutators with "get" and "set" class Rectangle { int width, height; public: Rectangle(int w = 0, int h = 0) : width(w), height(h) {} int getWidth() const { return width; } void setWidth(int w) { width = w; } int getHeight() const { return height; } void setHeight(int h) { height = h; } }; int main() { Rectangle r(19, 47); // Change width & height: r.setHeight(2 * r.getWidth()); r.setWidth(2 * r.getHeight()); } ///:~
Listado 9.5. C09/Rectangle2.cpp
Por supuesto, los accesores y mutadores no tienen porqué ser
simples tuberías hacia las variables internas. A veces,
pueden efectuar cálculos más sofisticados. El siguiente
ejemplo usa las funciones de tiempo de la librería C
estándar para crear una clase Time
:
//: C09:Cpptime.h // A simple time class #ifndef CPPTIME_H #define CPPTIME_H #include <ctime> #include <cstring> class Time { std::time_t t; std::tm local; char asciiRep[26]; unsigned char lflag, aflag; void updateLocal() { if(!lflag) { local = *std::localtime(&t); lflag++; } } void updateAscii() { if(!aflag) { updateLocal(); std::strcpy(asciiRep,std::asctime(&local)); aflag++; } } public: Time() { mark(); } void mark() { lflag = aflag = 0; std::time(&t); } const char* ascii() { updateAscii(); return asciiRep; } // Difference in seconds: int delta(Time* dt) const { return int(std::difftime(t, dt->t)); } int daylightSavings() { updateLocal(); return local.tm_isdst; } int dayOfYear() { // Since January 1 updateLocal(); return local.tm_yday; } int dayOfWeek() { // Since Sunday updateLocal(); return local.tm_wday; } int since1900() { // Years since 1900 updateLocal(); return local.tm_year; } int month() { // Since January updateLocal(); return local.tm_mon; } int dayOfMonth() { updateLocal(); return local.tm_mday; } int hour() { // Since midnight, 24-hour clock updateLocal(); return local.tm_hour; } int minute() { updateLocal(); return local.tm_min; } int second() { updateLocal(); return local.tm_sec; } }; #endif // CPPTIME_H ///:~
Listado 9.6. C09/Cpptime.h
Las funciones de la librería C estándar tienen múltiples
representaciones para el tiempo, y todas ellas son parte de
la clase Time
. Sin embargo, no es
necesario actualizar todos ellos, así que
time_t
se usa para la representación base, y
tm
local
y la representación
ASCII asciiRep
tienen banderas para
indicar si han sido actualizadas para el time_t
actual. Las dos funciones privadas
updateLocal()
y
updateAscii()
comprueban las banderas y
condicionalmente hacen la actualización.
El constructor llama a la función
mark()
(que el usuario puede llamar
también para forzar al objeto a representar el tiempo
actual), y eso limpia las dos banderas para indicar que el
tiempo local y la representación ASCII ya no son válidas. La
función ascii()
llama a
updateAscii()
, que copia el resultado
de la función de la librería estándar de C
asctime()
en un buffer local porque
asctime()
usa una área de datos
estática que se sobreescribe si la función se llama en otra
parte. El valor de retorno de la función
ascii()
es la dirección de ese buffer
local.
Todas las funciones que empiezan con
daylightSavings()
usan la función
updateLocal()
, que causa que la
composición resultante de inlines sea bastante larga. No
parece que valga la pena, especialmente considerando que
probablemente no quiera llamar mucho a esas funciones. Sin
embargo, eso no significa que todas las funciones deban ser
no-inline. Si hace otras funciones no-inline, al menos
mantenga updateLocal()
como
inline
de modo que su código se duplique en las
funciones no-inline, eliminando la sobrecarga extra de
invocación de funciones.
Este es un pequeño programa de prueba:
//: C09:Cpptime.cpp // Testing a simple time class #include "Cpptime.h" #include <iostream> using namespace std; int main() { Time start; for(int i = 1; i < 1000; i++) { cout << i << ' '; if(i%10 == 0) cout << endl; } Time end; cout << endl; cout << "start = " << start.ascii(); cout << "end = " << end.ascii(); cout << "delta = " << end.delta(&start); } ///:~
Listado 9.7. C09/Cpptime.cpp
Se crea un objeto Time
, se hace alguna
actividad que consuma tiempo, después se crea un segundo objeto
Time
para marcar el tiempo de
finalización. Se usan para mostrar los tiempos de inicio, fin
y los intervalos.