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() normalmente incluiría colocar this 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, eliminando la sobrecarga y permitiendo al usuario acceder directamente a i. Desde el punto de vista del diseño, eso resulta desastroso, porque i pasaría a formar 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 a su antojo.
Además, el uso de métodos para controlar atributos le permite añadir código al método para detectar cuándo 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 del constructor (brevemente introducida en el Capítulo 8 y ampliamente cubierta en el Capítulo 14) para inicializar wide y high (usando el formato de pseudoconstructor para los tipos de datos predefinidos).
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 principio. Sin embargo, los identificadores que empiezan con guión bajo están reservados y no debería usarlos.
En su lugar, puede optar por usar «get» y «set» 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 por qué 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 actualizarlas todas, 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 un á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 a partir de daylightSavings() usan la función updateLocal(), lo que hace que las inline compuestas resultantes sean bastante grandes. No parece que valga la pena, especialmente considerando que probablemente no 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.
Éste 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.