Typedef Literacy

NOTE: If you’re writing ≥ C++11, reach for type alias rather than typedef declaration. The syntax matches what people naturally expect, and it also supports templated type aliases (i.e. alias template).

Introduction

The typedef declaration provides a way to create an alias for an existing type. For example, we can provide an alias for int called integer like so:

typedef int integer;

I imagine most people have seen such declarations, and they are fairly simple to read. In fact it’s so simple that we may incorrectly conclude that the syntax for typedef is:

typedef <existing_type> <new_type_name>;

This syntax works for simple cases, but at some point we encounter a typedef declaration that doesn’t quite meet our expectations:

typedef int (*function_pointer)(double);

This declares an alias for int (*)(double) (pointer to function) called function_pointer. This is clearly inconsistent with our earlier conclusion, since if it were, we would have seen the following instead:

typedef int (*)(double) function_pointer;

In an effort to keep the simple cases simple, many people understandably start to treat this as a special case. Until they encounter another weird one:

typedef int array[3];

Fine! Yet another special case. But what about…

typedef int integer, (*function_pointer)(double), array[3];

Huh…? Is this even legal? — Yes. Yes it is. — I’m NOT suggesting that you do this!

Seriously, how do we read these damn things?

Correct Reading

The misconception and mystery behind the reading of the typedef declaration lies in the seemingly innocent, but misleading simple examples. Here is the syntax for the typedef declaration according to cppreference:

typedef type_declaration;

The type_declaration after the typedef keyword is a simple declaration with some restrictions (e.g. cannot be declared static).

Refer to the Standardese section if you’re interested in some of the fine details.

Consider the following variable declarations:

int i;
int (*f)(double);
int a[3];

In variable declarations, the introduced names are instances of the corresponding types.

int i;             // `i` is an instance of `int`.
int (*f)(double);  // `f` is an instance of `int (*)(double)`.
int a[3];          // `a` is an instance of `int [3]`.

However, when the typedef keyword precedes the declaration, the introduced names are aliases of the corresponding types.

typedef int i;             // `i` is an alias of `int`.
typedef int (*f)(double);  // `f` is an alias of `int (*)(double)`.
typedef int a[3];          // `a` is an alias of `int [3]`.

Thus, the task of reading a typedef declaration can be delegated to reading a variable declaration then reinterpreting the names to be aliases of the corresponding types as opposed to instances of them!

Inaccurate Sources

Unfortunately, the source of the misunderstanding goes beyond us naively assuming the simple syntax.

  • Wikipedia article enumerates a bunch of scenarios. It makes it seem as if there is a special case for function pointers, for example.

Function pointers are somewhat different than all other types because the syntax does not follow the pattern typedef <old type name> <new alias>;. Instead, the new alias for the type appears in the middle between the return type (on the left) and the argument types (on the right). […]

In C++, there are two syntaxes for creating such type aliases: The first, inherited from the C language, uses the typedef keyword:

typedef existing_type new_type_name ;

where existing_type is any type, either fundamental or compound, and new_type_name is an identifier with the new name given to the type.

Conclusion

Hopefully I’ve helped you understand how typedef declarations actually work. As I mentioned at the beginning of the post, reach for type alias instead if you’re writing modern C++.

As we’ve seen in the earlier examples, the design of typedef declaration successfully achieves the “make simple things simple” principle. However, the generalized rule that one would naturally deduce from the simple cases do not prepare you for the complex cases at all.

I’ve also shown that the underlying rule is actually quite simple and consistent. The level of confusion that arise from what seems like such a coherent and simple design is quite intriguing.

Standardese

The formal definition of the syntax can be found in §7 Declaration:

  • §7 ¶1:
  • §7 ¶9: If the decl-specifier-seq contains the typedef specifier, the declaration is called a typedef declaration and the name of each init-declarator is declared to be a typedef-name, synonymous with its associated type (7.1.3). […]

  • §7.1 ¶1:

  • §7.1.3 ¶1: Declarations containing the decl-specifier typedef declare identifiers that can be used later for naming fundamental (3.9.1) or compound (3.9.2) types. The typedef specifier shall not be combined in a decl-specifier-seq with any other kind of specifier except a defining-type-specifier, […] If a typedef specifier appears in a declaration without a declarator, the program is ill-formed.

    A name declared with the typedef specifier becomes a typedef-name. Within the scope of its declaration, a typedef-name is syntactically equivalent to a keyword and names the type associated with the identifier in the way described in Clause 8. A typedef-name is thus a synonym for another type. A typedef-name does not introduce a new type the way a class declaration (9.1) or enum declaration does. […]

Thanks to Artem Harutyunyan and Kapil Arya for reviewing this post!