9.2.2. Funciones de acceso

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.

Accesores y mutadores

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.