C++ Clases II

[forCode]

En este post, veremos los cuatro mecanismos principales de los que disponemos en C++ para definir nuevos tipos de datos: typedefs, enums, structs y clases. Luego nos centraremos en los dos últimos y veremos sus semejanzas, y sus diferencias.

Definiciones de tipos con typedef

La palabra clave typedef es el elemento más básico del que disponemos en C++ para definir un nuevo tipo de datos. De hecho typedef no permite crear un nuevo tipo, si no dar un alias a un tipo ya existente:

typedef int wparam;

Este código declara el tipo wparam que es un alias de int. Es decir, se trata realmente del mismo tipo, pero ahora puedo declarar variables y parámetros de tipo wparam (aunque realmente son de tipo int).

El uso de typedef es interesante porque permite la creación «de un conjunto de tipos virtuales», que en función de condiciones que se evalúan en el preprocesador, se mapea a uno u otro tipo de datos real. Eso facilita la creación de aplicaciones multiplataforma:

#ifdef _UNICODE
typedef wchar_t TCHAR
#else
typedef char TCHAR
#endif

Este código declara el tipo TCHAR como wchar_t si la constante del preprocesador _UNICODE existe, y si no, declara TCHAR como un char. De este modo declaramos el tipo de datos TCHAR que realmente será wchar_t en las plataformas que soporten Unicode (wchar_t es el tipo que se usa para contener caracteres Unicode) y char en las plataformas que no soporten Unicode. Si trabajamos siempre con TCHAR podemos atacar plataformas Unicode y no Unicode con el mismo código. Los propios compiladores definen constantes de preprocesador en función de la plataforma para la que compilan el código. P. ej. Visual Studio define la constante _WIN64 solo cuando compila código para x64.

Definición de tipos con enum

En C++ el uso de enum permite definir un conjunto de valores relacionados. Así podríamos declarar una enumeración que contenga los tres colores primarios:

enum Color { RED, GREEN, BLUE };

Los valores de un enum se pueden convertir al tipo subyacente que suele ser un int. Por defecto, el primer valor del enum es 0 y los siguientes se incrementa en uno:

int i = Color::GREEN;       // i vale 1

Las conversiones de un elemento int al Color correspondiente se realizan mediante static_cast (aunque un cast clásico se permite por compatibilidad):

Color c = static_cast<Color>(1);    // c vale Color::GREEN
Color c = (Color)1;              // c vale Color::GREEN (cast clásico)

C++11 bonus track: enum class

C++11 trajo una mejora a los enums clásicos, las enums classes. Una enum class es, básicamente, un enum pero que no se convierte automáticamente al tipo de datos subyacente.

enum class Color { RED, GREEN, BLUE };
int i = Color::RED;                      // Error.

El código falla porque ahora no hay conversión entre la enum class y el tipo subyacente (int). Si deseamos esta conversión debemos usar static_cast:

int a = static_cast<int>(Color::GREEN);    // a vale 1

Esto parece que solo añade complejidad, pero realmente previene posibles errores, de ahí que se recomiende siempre usar enum class en lugar de enum. Imagina el siguiente código:

enum Color { RED, GREEN, BLUE };
enum ColorCYM { CYAN, YELLOW, MAGENTA };
int a = Color::GREEN;
auto b = a == ColorCYM::YELLOW;

El valor de b es true, ya que a vale 1 (Color::GREEN se convierte a su valor entero) y luego se compara el valor entero de ColorCYM::YELLOW (que es 1) con el valor de a que era 1. En cambio usando enum class el código no compila:

enum class Color { RED, GREEN, BLUE };
enum class ColorCYM { CYAN, YELLOW, MAGENTA };
int a = static_cast<int>(Color::GREEN);
auto b = a == ColorCYM::YELLOW;

La última línea de un error, indicando que no puede compararse un int con un ColorCYM.

Otra ventaja de las enums class es que para acceder a sus valores se requiere el uso del operador de ámbito (::) cosa que no ocurre con los enums tradicionales. Se evita así colisiones de nombres:

enum  Color { RED, GREEN, BLUE };
enum class ColorCYM { CYAN, YELLOW, MAGENTA };
int r = RED;      // r vale 0 (Al ser enum es equivalente a r = Color::RED)
int y = CYAN;     // No compila. Al ser enum class debe usarse forzosamente ColorCYM::CYAN

structs y clases

Vale… vamos a tratar conjuntamente structs y clases. Antes que nada hagamos una lista de las diferencias que tienen, en C++, las clases y las structs:

  • Una clase puede tener métodos y campos. Una struct también.
  • Una clase puede tener métodos públicos, protegidos y privados. Una struct también.
  • Una clase puede derivar de otras clases. Una struct también.
  • Una clase puede pasarse por referencia o por valor. Una struct tambien.
  • Una clase puede tener métodos abstractos. Una struct también.
  • Una clase puede redefinir métodos definidos en la clase base. Una struct también

No hay (casi) ninguna diferencia en C++ entre clases y structs. Son lo mismo. No hay ninguna razón para preferir declarar un tipo como una clase o como una struct (más allá de fobias o filias personales). De hecho el siguiente código es totalmente equivalente:

class CFoo {
	// código
};
struct SFoo {
private:
	// código
};

Y es que la única diferencia entre struct y class es que en la primera el ámbito de visibilidad por defecto es «public». Mientras que en una clase es «private». Es decir, si no indicamos lo contrario, los campos y métodos declarados en una struct son públicos, mientras que en una clase son, por defecto, privados. Quizá pienses que o bien structs o bien clases sobran en C++ y tienes razón. El hecho de que existan ambas es por compatibilidad con C, que soportaba structs (entendiendo por struct un conjunto de campos (variables), pero sin la posibilidad de tener métodos propios). Cuando crearon C++ se decidió mantener el soporte a structs (para compatibilidad con C) pero se pensó que la palabra clave «class» definía mejor el hecho de crear una clase y se incorporó al lenguaje. Pero al mismo tiempo se decidió potenciar las structs y dotarlas de todos los mecanismos que tenían las clases. El que, por defecto los campos (o métodos) de una struct sean públicos y no privados, es de nuevo por compatibilidad con código C, donde las structs no tienen ámbitos de visibilidad (y todo es público).

typedef struct o ese viejo amigo

En C es muy normal combinar typedef con struct. Es decir tener el siguiente código:

typedef struct Point {
	int x;
	int y;
} Point;

Este código parece un sinsentido: declara una struct llamada Point y luego usa typedef para crear un alias llamado… Point. ¿Por qué hacer esto? ¿Por qué no simplemente declarar la struct y olvidarse del typedef? Por supuesto, puedes hacerlo pero entonces en C prepárate para un molesto detalle:

struct Point {
	int x;
	int y;
};
Point a;       // Error. En C eso NO COMPILA

Este código, en C, no compila y da un error de que Point no está definido. Y es que en C, las structs no declaran tipos completos, de hecho el tipo no es Point, es struct Point. Así debemos declarar la variable a, no como «Point a» si no como «struct Point a». Del mismo modo si tenemos algún parámetro de tipo Point, éste debemos declararlo de tipo «struct Point». Como esto es bastante molesto, se suele usar un typedef, para pasar de «struct Point» a «Point» a secas.

Esto en C++ no es necesario, ya que las structs sí que declaran tipos completos, de forma que en el ejemplo anterior «Point a;» es una declaración válida de variable. El typedef sobra (pero no molesta) en C++, pero muchas veces te lo vas a encontrar, así quien es importante que sepas la razón.

Bueno… en este post hemos repasado los mecanismos básicos para crear tipos en C++… en el siguiente ya nos adentraremos en los detalles de las clases (y de las structs, pues como hemos visto son básicamente lo mismo).