8.4.3. Objetos y métodos constantes

Las funciones miembro (métodos) se pueden hacer constantes. ¿Qué significa eso? Para entenderlo, primero debe comprender el concepto de objeto constante.

Un objeto constante se define del mismo modo para un tipo definido por el usuario que para un tipo del lenguaje. Por ejemplo:

const int i = 1;
const blob b(2);

Aquí, b es un objeto constante de tipo blob, su constructor se llama con un 2 como argumento. Para que el compilador imponga que el objeto sea constante, debe asegurar que el objeto no tiene atributos que vayan a cambiar durante el tiempo de vida del objeto. Puede asegurar fácilmente que los atributos no públicos no sean modificables, pero. ¿Cómo puede saber que métodos cambiarán los atributos y cuáles son seguros para un objeto constante?

Si declara un método como constante, le está diciendo que la función puede ser invocada por un objeto constante. Un método que no se declara constante se trata como uno que puede modificar los atributos del objeto, y el compilador no permitirá que un objeto constante lo utilice.

Pero la cosa no acaba ahí. Sólo porque un método afirme ser const no garantiza que actuará del modo correcto, de modo que el compilador fuerza que en la definición del método se reitere el especificador const (la palabra const se convierte en parte del nombre de la función, así que tanto el compilador como el enlazador comprobarán que no se viole la constancia). De este modo, si durante la definición de la función se modifica algún miembro o se llama algún método no constante, el compilador emitirá un mensaje de error. Por eso, está garantizado que los miembros que declare const se comportarán del modo esperado.

Para comprender la sintaxis para declarar métodos constantes, primero debe recordar que colocar const delante de la declaración del método indica que el valor de retorno es constante, así que no produce el efecto deseado. Lo que hay que hacer es colocar el especificador const después de la lista de argumentos. Por ejemplo:

//: C08:ConstMember.cpp
class X {
  int i;
public:
  X(int ii);
  int f() const;
};

X::X(int ii) : i(ii) {}
int X::f() const { return i; }

int main() {
  X x1(10);
  const X x2(20);
  x1.f();
  x2.f();
} ///:~

Listado 8.14. C08/ConstMember.cpp


La palabra const debe incluirse tanto en la declaración como en la definición del método o de otro modo el compilador asumirá que es un método diferente. Como f() es un método constante, si intenta modificar i de alguna forma o llamar a otro método que no sea constante, el compilador informará de un error.

Puede ver que un miembro constante puede llamarse tanto desde objetos constantes como desde no constantes de forma segura. Por ello, debe saber que esa es la forma más general para un método (a causa de esto, el hecho de que los métodos no sean const por defecto resulta desafortunado). Un método que no modifica ningún atributo se debería escribir como constante y así se podría usar desde objetos constantes.

Aquí se muestra un ejemplo que compara métodos const y métodos ordinarios:

//: C08:Quoter.cpp
// Random quote selection
#include <iostream>
#include <cstdlib> // Random number generator
#include <ctime> // To seed random generator
using namespace std;

class Quoter {
  int lastquote;
public:
  Quoter();
  int lastQuote() const;
  const char* quote();
};

Quoter::Quoter(){
  lastquote = -1;
  srand(time(0)); // Seed random number generator
}

int Quoter::lastQuote() const {
  return lastquote;
}

const char* Quoter::quote() {
  static const char* quotes[] = {
    "Are we having fun yet?",
    "Doctors always know best",
    "Is it ... Atomic?",
    "Fear is obscene",
    "There is no scientific evidence "
    "to support the idea "
    "that life is serious",
    "Things that make us happy, make us wise",
  };
  const int qsize = sizeof quotes/sizeof *quotes;
  int qnum = rand() % qsize;
  while(lastquote >= 0 && qnum == lastquote)
    qnum = rand() % qsize;
  return quotes[lastquote = qnum];
}

int main() {
  Quoter q;
  const Quoter cq;
  cq.lastQuote(); // OK
//!  cq.quote(); // Not OK; non const function
  for(int i = 0; i < 20; i++)
    cout << q.quote() << endl;
} ///:~

Listado 8.15. C08/Quoter.cpp


Ni los constructores ni los destructores pueden ser métodos constantes porque prácticamente siempre realizan alguna modificación en el objeto durante la inicialización o la terminación. El miembro quote() tampoco puede ser constante porque modifica el atributo lastquote (ver la sentencia de retorno). Por otra parte lastQuote() no hace modificaciones y por eso puede ser const y puede ser llamado de forma segura por el objeto constante cq.

mutable: constancia binaria vs. lógica

¿Qué ocurre si quiere crear un método constante, pero necesita cambiar algún atributo del objeto? Esto se aplica a veces a la diferencia entre constante binaria (bitwise) y constante lógica (llamado también constante memberwise). Constante binaria significa que todos los bits del objeto son permanentes, así que la imagen binaria del objeto nunca cambia. Constante lógica significa que, aunque el objeto completo es conceptualmente constante puede haber cambios a nivel de miembro. Si se informa al compilador que un objeto es constante, cuidará celosamente el objeto para asegurar constancia binaria. Para conseguir constancia lógica, hay dos formas de cambiar los atributos con un método constante.

La primera solución es la tradicional y se llama constancia casting away. Esto se hace de un modo bastante raro. Se toma this (la palabra que inidica la dirección del objeto actual) y se moldea el puntero a un puntero a objeto de la clase actual. Parece que this ya es un puntero válido. Sin embargo, dentro de un método constante, this es en realidad un puntero constante, así que moldeándolo a un puntero ordinario se elimina la constancia del objeto para esta operación. Aquí hay un ejemplo:

//: C08:Castaway.cpp
// "Casting away" constness

class Y {
  int i;
public:
  Y();
  void f() const;
};

Y::Y() { i = 0; }

void Y::f() const {
//!  i++; // Error -- const member function
  ((Y*)this)->i++; // OK: cast away const-ness
  // Better: use C++ explicit cast syntax:
  (const_cast<Y*>(this))->i++;
}

int main() {
  const Y yy;
  yy.f(); // Actually changes it!
} ///:~

Listado 8.16. C08/Castaway.cpp


Esta aproximación funciona y puede verse en código correcto, pero no es la técnica ideal. El problema es que esta falta de constancia está oculta en la definición de un método y no hay ningún indicio en la interfaz de la clase que haga sospechar que ese dato se modifica a menos que puede accederse al código fuente (buscando el molde). Para poner todo al descubierto se debe usar la palabra mutable en la declaración de la clase para indicar que un atributo determinado se puede cambiar aún perteneciendo a un objeto constante.

//: C08:Mutable.cpp
// The "mutable" keyword

class Z {
  int i;
  mutable int j;
public:
  Z();
  void f() const;
};

Z::Z() : i(0), j(0) {}

void Z::f() const {
//! i++; // Error -- const member function
    j++; // OK: mutable
}

int main() {
  const Z zz;
  zz.f(); // Actually changes it!
} ///:~

Listado 8.17. C08/Mutable.cpp


De este modo el usuario de la clase puede ver en la declaración qué miembros tienen posibilidad de ser modificados por un método.

ROMability

Si un objeto se define como constante es un candidato para ser almacenado en memoria de sólo lectura (ROM), que a menudo es una consideración importante en programación de sistemas empotrados. Para conseguirlo no es suficiente con que el objeto sea constante, los requisitos son mucha más estrictos. Por supuesto, el objeto debe ser una constante binaria. Eso es fácil de comprobar si la constancia lógica se implementa mediante el uso de mutable, pero probablemente el compilador no podrá detectarlo si se utiliza la técnica del moldeado dentro de un método constante. Además:

  • La clase o estructura no puede tener constructores o destructor definidos por el usuario.

  • No pueden ser clases base (capitulo 14) u objetos miembro con constructores o destructor definidos por el usuario.

El efecto de una operación de escritura en una parte del objeto constante de un tipo ROMable no está definido. Aunque un objeto pueda ser colocado en ROM de forma conveniente, no todos lo requieren.