I wanted to know what advantage do we get by typedefs?
In many ways, none at all.
In one somewhat important way, a fairly big one.
The underlying problem / solution here is "abstraction".
Fundamentally, abstraction is all about removing unnecessary
detail, while retaining necessary detail. (Not all abstractions
*succeed* at this task, mind.) This is really the heart of
most (maybe even all) computer programming.
The real world is ineluctably complex. Computer programs tend to
deal with simplified models. Even so, the simplified models are
often (maybe even usually) sufficiently complicated that programmers
cannot debug them without breaking them down even further. This
is why we use higher level languages -- it is at least one reason,
and I think one can argue that it is really the only reason, that
we do not write everything in machine code -- and also why we
break large programs into functions and (in langauges like C)
data structures.
C has a number of mostly-concrete data types: char, short, int,
and long (and in C99 long long) and their unsigned variants; float,
double, and long double; and pointers. (C99 adds complex number
types. C also has "void", but this is mostly a degenerate case,
and "enum", but enum is just a special case of "integral".) These
types are already somewhat abstracted from the underlying machine;
for instance, many microprocessors do not have hardware support
(or full support) for 64 or even 32 bit integral types, and many
only offer floating-point as an option (with a coprocessor) and/or
need software assistance. Still, these types are generally "close
to the metal", as the phrase goes: most CPUs implement most of them
mostly in hardware (although complex numbers rarely have direct
hardware support).
In addition to these (and derived types -- which actually include
all the pointer types as well as arrays; note also that "pointer
to function returning T" is a data type, even though it points to
a function type), C has the "user-defined abstract data type",
which is spelled "struct". Whenever you declare or define a new
struct, you get a new type. This new type is not compatible with
any existing type:
struct one { int val; } a;
struct two { int val; } b;
...
a = b; /* ERROR, type mismatch */
User-defined types give you everything you need to make new,
compiler-checked abstractions. A subroutine or function is also
an abstraction, but unless you make it use or return a user-defined
data type, it is possible to apply it to data of an inappropriate
type. Consider, for instance, a subroutine that checks a
temperature, which you might use in the control system for a
nuclear reactor:
void check_temperature(double the_value) { ... }
If you are only checking "any old double" (as in this case), it is
possible to call this with a length or pressure measurement by
accident:
double x;
...
x = measure_something(...); /* actually returns a pressure */
...
check_temperature(x);
A C compiler is not required to complain, and it would be surprising
to find one that does. Make separate "temperature" and "pressure"
data types, however, and we get:
void check_temperature(struct temperature the_value) { ... }
...
struct pressure x;
...
x = measure_something(...);
...
check_temperature(x);
Now the compiler *is* required to complain.
The problem with C's "typedef" is that it does *not* actually define
types. Instead, it just defines an alias for some existing type.
The aliases can be mixed freely. Thus if we try to use:
typedef double temperature;
typedef double pressure;
we lose many of the advantages of abstract data types.
At the same time, however, typedefs *do* give us a level of
indirection. Consider ANSI/ISO C "size_t", for instance. The
Standard tells us that size_t is an unsigned integral type that
holds the size (in bytes, which C calls "char"s) of an object. In
general, size_t is an alias for one of three types: unsigned int,
unsigned long, or unsigned long long. (Technically it could be an
alias for unsigned char or unsigned short, or even one of the weird
"extra" types allowed in C99, but in practice this does not occur.)
This "level of indirection" acts as a sort of "leaky" abstract
type. It fails to provide compile-time type-checking; we can use
the wrong types all over the place and never even notice. But *if*
we manage to use it correctly, it does insulate us from any changes
needed when porting code from one machine to another. If size_t
should be "unsigned int" on one 64-bit machine, but "unsigned long"
on another, it *can* be. We can -- if we are sufficiently careful
-- avoid assuming that it is either one or the other.
The two things typedef gives you, that struct does not, are:
- you do not need to write out the word "struct" each time, and
- you can make the type-name a synonym for a fundamental, built
in type (like "long" or "unsigned char"), rather than a
user-defined abstract type.
In C89 (but not C99), that second point is significant, because
there is no way in C89 to write constants of user-defined type.
In C99 we can do things like this:
struct pressure { double val; };
#define PRESSURE_UNKNOWN ((struct pressure){-1})
...
struct pressure p = estimate_pressure(...);
...
some_loop_construct {
...
if (some_condition)
p = PRESSURE_UNKNOWN; /* make sure we measure it below */
...
if (p == PRESSURE_UNKNOWN) ...
...
}
(Of course, in C89 you can always write macros to deal with
this, e.g., #define SET_TO_UNKNOWN(pp) ((pp)->val = -1), but
this is hardly elegant.)
Structure values are often implemented relatively inefficiently.
If this is the case for any particular situation / compiler, we
can use typedef to get a "checked system", and then recompile to
get a (presumably faster) "unchecked" version:
#ifdef SLEAZE
typedef double pressure;
#define MK_CONSTANT(t, val) ((t)(val))
#else
typedef struct pressure pressure;
struct pressure { double val; };
#define MK_CONSTANT(t, val) ((t){val})
#endif
...
pressure estimate_pressure(...);
#define PRESSURE_UNKNOWN MK_CONSTANT(pressure, -1)
Now we simply need to "#define SLEAZE" to get the "unchecked"
version. (The MK_CONSTANT trick above is again C99-dependent.)