User-Defined Types

Enumerations

Enumerations allow compile-time constants to be defined, and automatically increment each constant from the one that precedes it. For example,

enum color {
   COL_RED,
   COL_GREEN,
   COL_BLUE
};

defines COL_RED as 0, COL_GREEN as 1, and COL_BLUE as 2. Notice that enumeration constants are always written in all capitals, and generally have some unambiguous prefix.

Values can also be assigned to specific constants; auto-numbering of successive constants continues from the previous value. Previously declared constants may be used in value assignment as well. As an example,

enum wheelnum {
   WN_UNICYCLE = 1,
   WN_BICYCLE,  /* 2 */
   WN_MOTORCYCLE = WN_BICYCLE,
   WN_TRICYCLE, /* 3 */
   WN_CAR,      /* 4 */
   WN_TRAILER = 6
};

One important difference between enumerations and regular variables is that enumerations can be part of compile-time constant expressions. Sometimes these are used for non-enumerative purposes,

enum config {
   CFG_BUF_SIZE = 1024,
   /* etc ... */
};

int buffer[CFG_BUF_SIZE];

The following would not work, because array sizes must be compile-time constants:

int CFG_BUF_SIZE = 1024;
int buffer[CFG_BUF_SIZE]; /* ERROR: Array size is not a compile-time constant */

Students often expect const-qualified variables to behave like true enumeration constants, but this is not the case. As explained earlier, const-qualification is only a promise not to modify an object. The program is free to violate that promise and cause undefined behavior as a result of the compiler incorrectly assuming the object’s value never changes. Therefore, this still will not work,

int const CFG_BUF_SIZE = 1024;
int buffer[CFG_BUF_SIZE]; /* ERROR: Array size is not a compile-time constant */

Structures

Structures are frequently used for storing structured data,

struct cartesian {
   int x;
   int y;
};
struct cartesian player_coord, monster_coord;

It is also common to declare type aliases for structures,

typedef struct cartesian cartesian_t;
cartesian_t coords;

…sometimes without declaring a tag name at all,

typedef struct {
   /* ... */
} some_type_t;

Structures can also be nested,

struct car {
   struct wheel {
      int width;
      int diameter;
   } wheels[4];

   enum fuel_type {
      FUEL_GAS,
      FUEL_DIESEL,
      FUEL_ELECTRIC,
      FUEL_HYBRID
   } fuel_type;
/* ... */

Unions

Unions are a more specialty concept and are generally used for three purposes: type punning, polymorphism, and storage reuse.

Type punning

Type punning is a special property of a union that allows data stored as one type to be accessed as if it were another type. This is a direct re-interpretation on the byte level, so it requires an understanding of how both types are laid out in memory on a particular implementation–otherwise it can lead to unexpected behavior. Here is one common example,

union cpu_reg {
   uint32_t word;    /* Access to whole 32-bit word */
   uint8_t bytes[4]; /* Access to each 8-bit byte */
};

Polymorphism

Unions can also be used for run-time polymorphism by being able to hold values of different types. These are generally embedded in a structure that has another member to store dynamic type information. For example,

struct char_or_int {
   enum {
      TYPE_INT,
      TYPE_CHAR
   } type;
   union {
      int as_int;
      char as_char;
   } data;
}

Storage reuse

Unions are also often used for storage reuse with static objects. In this usage, the union object is used as one data type, then another, at different points in the program; it is similar to dynamic polymorphism, except in this case the type the object is being used as is always the same at the same point in the program, or deduced from other state information.

For example, suppose that a game engine is processing objects one at a time, and it stores the current object id and a pointer to that object in static storage,

static int current_object_id;
static object_t *current_object;

This requires storage for both an int and an object_t* for the duration of the program.

Suppose, however, that for some portion of the program the object id is needed, and for another portion an object pointer is needed. In this case, a union can be used to save memory,

static union {
   int id;
   object_t *p;
} current_object;