7.3. Uniones

Como ya ha visto, la única diferencia en C++ entre struct y class es que struct pone todo por defecto a public y la clase pone todo por defecto a private. Una struct también puede tener constructores y destructores, como cabía esperar. Pero resulta que el tipo union también puede tener constructores, destructores, métodos e incluso controles de acceso. Puede ver de nuevo la utilización y las ventajas de la sobrecarga de funciones en el siguiente ejemplo:

//: C07:UnionClass.cpp
// Unions with constructors and member functions
#include<iostream>
using namespace std;

union U {
private: // Access control too!
  int i;
  float f;
public:  
  U(int a);
  U(float b);
  ~U();
  int read_int();
  float read_float();
};

U::U(int a) { i = a; }

U::U(float b) { f = b;}

U::~U() { cout << "U::~U()\n"; }

int U::read_int() { return i; }

float U::read_float() { return f; }

int main() {
  U X(12), Y(1.9F);
  cout << X.read_int() << endl;
  cout << Y.read_float() << endl;
} ///:~

Listado 7.6. C07/UnionClass.cpp


Podría pensar que en el código anterior la única diferencia entre una unión y una clase es la forma en que los datos se almacenan en memoria (es decir, el int y el float están superpuestos). Sin embargo una unión no se puede utilizar como clase base durante la herencia, lo cual limita bastante desde el punto de vista del diseño orientado a objetos (veremos la herencia en el Capítulo 14).

Aunque los métodos civilizan ligeramente el tratamiento de uniones, sigue sin haber manera alguna de prevenir que el programador cliente seleccione el tipo de elemento equivocado una vez que la unión se ha inicializado. En el ejemplo anterior, podría escribir X.read_float() incluso aunque sea inapropiado. Sin embargo, una unión «segura» se puede encapsular en una clase. En el siguiente ejemplo, vea cómo la enumeración clarifica el código, y cómo la sobrecarga viene como anillo al dedo con los constructores:

//: C07:SuperVar.cpp
// A super-variable
#include <iostream>
using namespace std;

class SuperVar {
  enum {
    character,
    integer,
    floating_point
  } vartype;  // Define one
  union {  // Anonymous union
    char c;
    int i;
    float f;
  };
public:
  SuperVar(char ch);
  SuperVar(int ii);
  SuperVar(float ff);
  void print();
};

SuperVar::SuperVar(char ch) {
  vartype = character;
  c = ch;
}

SuperVar::SuperVar(int ii) {
  vartype = integer;
  i = ii;
}

SuperVar::SuperVar(float ff) {
  vartype = floating_point;
  f = ff;
}

void SuperVar::print() {
  switch (vartype) {
    case character:
      cout << "character: " << c << endl;
      break;
    case integer:
      cout << "integer: " << i << endl;
      break;
    case floating_point:
      cout << "float: " << f << endl;
      break;
  }
}

int main() {
  SuperVar A('c'), B(12), C(1.44F);
  A.print();
  B.print();
  C.print();
} ///:~

Listado 7.7. C07/SuperVar.cpp


En ese ejemplo la enumeración no tiene nombre de tipo (es una enumeración sin etiqueta). Esto es aceptable si va a definir inmediatamente un ejemplar de la enumeración, tal como se hace aquí. No hay necesidad de indicar el nombre del tipo de la enumeración en el futuro, por lo que aquí el nombre de tipo es opcional.

La unión no tiene nombre de tipo ni nombre de variable. Esto se denomina unión anónima, y crea espacio para la unión pero no requiere acceder a los elementos de la unión con el nombre de la variable y el operador punto. Por ejemplo, si su unión anónima es:

//: C07:AnonymousUnion.cpp
int main() {
  union { 
    int i; 
    float f; 
  };
  // Access members without using qualifiers:
  i = 12;
  f = 1.22;
} ///:~

Listado 7.8. C07/AnonymousUnion.cpp


Note que accede a los miembros de una unión anónima igual que si fueran variables normales. La única diferencia es que ambas variables ocupan el mismo espacio de memoria. Si la unión anónima está en el ámbito del fichero (fuera de todas las funciones y clases), entonces se ha de declarar estática para que tenga enlace interno.

Aunque ahora SuperVar es segura, su utilidad es un poco dudosa porque la razón de utilizar una unión principalmente es la de ahorrar memoria y la adición de vartype hace que ocupe bastante espacio en la unión (relativamente), por lo que la ventaja del ahorro desaparece. Hay un par de alternativas para que este esquema funcione. Si vartype controlara más de una unión (en el caso de que fueran del mismo tipo) entonces sólo necesitaría uno para el grupo y no ocuparía más memoria. Una aproximación más útil es tener #ifdefs alrededor del código de vartype, el cual puede entonces garantizar que las cosas se utilizan correctamente durante el desarrollo y las pruebas. Si el código ha de entregarse, antes puede eliminar las sobrecargas de tiempo y memoria.