Type aliases using typedef

Suppose that you want to represent character strings as

struct string {
    int length;
    char *data;         /* malloc'd block */
};

int stringLength(const struct string *s);

If you later change the representation to, say, traditional null-terminated char * strings or some even more complicated type (union string **some_string[2];), you will need to go back and replace ever occurrence of struct string * in every program that uses it with the new type. Even if you don’t expect to change the type, you may still get tired of typing struct string * all the time, especially if your fingers slip and give you struct string sometimes.

The solution is to use a typedef, which defines a new type name:

typedef struct string *String;

int stringLength(const String s);

The syntax for typedef looks like a variable declaration preceded by typedef, except that the variable is replaced by the new type name that acts like whatever type the defined variable would have had. You can use a name defined with typedef anywhere you could use a normal type name, as long as it is later in the source file than the typedef definition. Typically typedefs are placed in a header file (.h file) that is then included anywhere that needs them.

You are not limited to using typedefs only for complex types. For example, if you were writing numerical code and wanted to declare overtly that a certain quantity was not just any double but actually a length in meters, you could write

typedef double LengthInMeters;
typedef double AreaInSquareMeters;

AreaInSquareMeters rectangleArea(LengthInMeters height, LengthInMeters width);

Unfortunately, C does not do type enforcement on typedef’d types: it is perfectly acceptable to the compiler if you pass a value of type AreaInSquareMeters as the first argument to rectangleArea, since by the time it checks it has replaced by AreaInSquareMeters and LengthInMeters by double. So this feature is not as useful as it might be, although it does mean that you can write rectangleArea(2.0, 3.0) without having to do anything to convert 2.0 and 3.0 to type LengthInMeters.

Opaque structs

There are certain cases where the compiler needs to know the definition of a struct:

  1. When the program accesses its components.
  2. When the compiler needs to know its size. This may be because you are building an array of these structs, because they appear in a larger struct, when you are passing the struct as an argument or assigning it to a variable, or just because you applied sizeof to the struct.

But the compiler does not need to know the definition of a struct to know how create a pointer to it. This is because all struct pointers have the same size and structure.

This allows a trick called an opaque struct, which can be used for information hiding, where one part of your program is allowed to see the definition of a struct but other parts are not.

The idea is to create a header file that defines all the functions that might be used to access the struct, but does not define the struct itself. For example, suppose we want to create a counter, where the user can call a function increment that acts like ++ in the sense that it increments the counter and returns the new value, but we don’t want to allow the user to change the value of the counter in any other way. This header file defines the interface to the counter.

Here is the header file:

/* Create a new counter, initialized to 0.  Call counterDestroy to get rid of it. */
struct counter * counterCreate(void);

/* Free space used by a counter. */
void counterDestroy(struct counter *);

/* Increment a counter and return new value. */
int counterIncrement(struct counter *);

examples/structs/opaqueStructs/counter.h

We can now write code that uses the struct counter * type without knowing what it is actually pointing to:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

#include "counter.h"

int
main(int argc, char **argv)
{
    struct counter *c;
    int value;

    c = counterCreate();

    while((value = counterIncrement(c)) < 10) {
        printf("%d\n", value);
    }

    counterDestroy(c);

    return 0;
}

examples/structs/opaqueStructs/testCounter.c

To make this work, we do have to provide an implementation. The obvious way to do it is have a struct counter store the counter value in an int, but one could imagine other (probably bad) implementations that did other things, as long as from the outside they acted like we expect.

We only put the definition of a struct counter in this file. This means that only functions in this file can access a counter’s components, compute the size of a counter, and so forth. While we can’t absolutely prevent some other function from extracting or modifying the contents of a counter (C doesn’t provide that kind of memory protection), we can at least hint very strongly that the programmer shouldn’t be doing this.

#include <stdlib.h>
#include <assert.h>

#include "counter.h"

struct counter {
    int value;
};

struct counter *
counterCreate(void)
{
    struct counter *c;

    c = malloc(sizeof(struct counter));
    assert(c);

    c->value = 0;

    return c;
}

void
counterDestroy(struct counter *c)
{
    free(c);
}

int
counterIncrement(struct counter *c)
{
    return ++(c->value);
}

examples/structs/opaqueStructs/counter.c

We will see this trick used over and over again when we build abstract data types in the next chapters.


Licenses and Attributions


Speak Your Mind