Proven C Book한국어 GitHub

59 Functions as values — the function pointer

What to know first

chapter 24, Declaring and defining functions · the name of a function
chapter 38, Arrays · a name decaying into a value

Looking back

Chapter 38 said an array name decays into the address of its first element, and chapter 58 said a variadic function loses type information. Then what does a function name evaluate to in an expression?

A. To a pointer to the function — the same decay as with arrays exists for functions. The exceptions are only when it is the operand of sizeof or &, and since applying sizeof to a function is forbidden to begin with, effectively & alone is the exception. And &function gives the same pointer in the end anyway. So the strange situation arises in which f, &f and *f are all the same value — which is this chapter’s first example.

The need for this chapter, and its context

Treating a function as a value needs two things first: what a function’s name evaluates to (chapter 24), and the rule of decay (chapter 38). With both behind us, this is the right slot. And it runs straight into the next chapter: the moment you make a function pointer the declarations turn ugly, and that is chapter 60′s subject.

By the end of this chapter

Until now a function has been “a thing you call”. In this chapter we handle a function as a value — put it in a variable, pass it as an argument, lay several out in an array. Along the way we see an old rule of C (a function name decays into a pointer), the strange syntax that rule makes, and how to build object orientation by hand with this tool.

The questions this chapter answers

  1. So is dlsym badly designed? If POSIX were being written today, how should it look?
  2. Then why not simply fix on one generic function pointer type that holds any function — say typedef void *(*generic_func_ptr_t)(void);?
  3. Then what differs from C++‘s virtual functions?

59.1 The name decays into a pointer

examples-en/ch59/funcptr.c

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

static int add(int a, int b) { return a + b; }
static int mul(int a, int b) { return a * b; }

/* a function taking a function pointer — the basic form of a callback */
static int apply(int (*op)(int, int), int a, int b) { return op(a, b); }

int main(void)
{
    /* (1) a function name decays to a pointer the moment it is used as a value */
    int (*p)(int, int) = add;     /* it works without & */
    int (*q)(int, int) = &add;    /* attaching & is the same */
    printf("add == &add : %s\n", p == q ? "same" : "different");

    /* (2) however many stars are attached the result is the same — the dereference decays again */
    printf("p(2,3)=%d  (*p)(2,3)=%d  (***p)(2,3)=%d  (*******p)(2,3)=%d\n",
           p(2, 3), (*p)(2, 3), (***p)(2, 3), (*******p)(2, 3));

    /* (3) & , on the other hand, can be used only once: &&add is a syntax error
          (&add is only a value, and the address of that value cannot be taken again) */

    /* (4) a dispatch table — choosing with an array instead of a switch */
    struct { const char *name; int (*fn)(int, int); } table[] = {
        { "add", add }, { "mul", mul },
    };
    for (size_t i = 0; i < sizeof table / sizeof table[0]; i++)
        printf("%s(6,7) = %d\n", table[i].name, apply(table[i].fn, 6, 7));

    /* (5) there is no guarantee that a function pointer is the size of a data pointer */
    printf("sizeof(void*)=%zu, sizeof(int(*)(int,int))=%zu\n",
           sizeof(void *), sizeof(int (*)(int, int)));

    /* (6) qsort's comparator is a function pointer too */
    int v[] = { 5, 2, 9, 1 };
    int cmp(const void *a, const void *b);   /* defined below */
    qsort(v, 4, sizeof v[0], cmp);
    printf("sorted: %d %d %d %d\n", v[0], v[1], v[2], v[3]);
    return 0;
}

int cmp(const void *a, const void *b)
{
    int x = *(const int *)a, y = *(const int *)b;
    return (x > y) - (x < y);
}

Output

add == &add : same
p(2,3)=5  (*p)(2,3)=5  (***p)(2,3)=5  (*******p)(2,3)=5
add(6,7) = 13
mul(6,7) = 42
sizeof(void*)=8, sizeof(int(*)(int,int))=8
sorted: 1 2 5 9

The first two lines of the output are this chapter’s heart.

add == &add — the function name decays into a pointer, and attaching & gives the same pointer. So the two are equal.

(*******p)(2,3) compiles — the reason any number of stars gives the same result is this. *p dereferences the pointer to obtain a function designator, and the moment that designator is used as a value it decays back into a pointer. That is, each * merely goes once round the loop “pointer → function → pointer” and stays where it was.

The opposite direction does not hold. &&add is a syntax error — &add is already a value (a pointer), and in C the address of a value cannot be taken (to take an address there must be a named place, that is, an lvalue). Hence the asymmetry that **add works while &&add does not.

A common misconception. “To call a function pointer you must dereference it, as in (*p)(x)

An old practice. p(x) and (*p)(x) are entirely the same and the standard permits both. The reason some codebases prefer (*p)(x) is the documentary purpose of telling the reader that this is a function pointer, not a requirement of the grammar. Either way, be consistent.

59.2 The type is the contract

A function pointer’s type is settled by the return type and the parameter list.

int  (*p)(int, int);       /* pointer to a function taking two ints, giving an int */
void (*q)(void);           /* pointer to a function with no arguments and no return */
int  (*r[4])(int);         /* an array of four such pointers */
int  (*(*s)(void))(int);   /* hard to read — use a typedef */

As the last line shows, declarations quickly turn rough. The practice in the field is typedef.

typedef int (*binop_fn)(int, int);
binop_fn table[] = { add, mul };

Casting to a function pointer of a different type and calling it is outside the contract. For example, something stored as void (*)(void) must not be called back as int (*)(int). Only storing and converting back to the original type to call is guaranteed.

Counter-example. Matching a comparator signature by casting

int cmp_int(const int *a, const int *b);            /* looks convenient, but */
qsort(v, n, sizeof v[0], (int (*)(const void *, const void *))cmp_int);

qsort passes two const void *, while the real function expects const int *. Since it is not guaranteed that the two types have the same representation, this call is outside the contract. The right way is to match the signature exactly and cast inside — as the example’s cmp did.

59.3 void * and function pointers are different worlds

Chapter 35 taught that void * is “a vessel that holds any data pointer”. Yet function pointers do not go into that vessel. The standard does not define conversion between data pointers and function pointers.

The reason lies in history and hardware. On a machine using a Harvard architecture, code and data are in different address spaces — the AVR microcontroller is representative, and there “address 0” exists separately in the code region and in the data region. Even the widths of the addresses may differ. On such a machine, putting the two pointers in the same vessel is impossible to begin with.

That is why the example printed the two sizes together. On this machine they happened to be equal, but there is no guarantee anywhere that they are.

That this split is not an academic worry has a proof at the heart of Unix. The POSIX function dlsym, which finds a function in a dynamic library, returns a void *, while what the caller wants is a function pointer — that is, a widely used API demands a conversion the standard does not define. This story is followed to its end later in the chapter, in the section on printing function pointers.

59.4 How to print a function pointer

The previous section said void * and function pointers are different worlds. The place that fact trips people up most often is logging — you want to record “which callback ran”, and chapter 35′s printf("%p", (void *)p) does not work here.

examples-en/ch59/print_funcptr.c

/* Printing a function pointer — three roads for a value %p will not take. */
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

static int add(int a, int b) { return a + b; }
static int sub(int a, int b) { return a - b; }
static int mul(int a, int b) { return a * b; }

typedef int (*binop)(int, int);

/* ── Road 1: lift the bytes and print them in hex ───────────────────
   Casting a function pointer to void* is outside the standard. Moving the
   bytes with memcpy is inside the contract everywhere — the size is all. */
static void fmt_funcptr(char *out, size_t cap, binop f)
{
    unsigned char raw[sizeof f];
    memcpy(raw, &f, sizeof raw);

    size_t k = 0;
    k += (size_t)snprintf(out + k, cap - k, "0x");
    for (size_t i = sizeof raw; i-- > 0 && k + 2 < cap; )
        k += (size_t)snprintf(out + k, cap - k, "%02X", raw[i]);
}

/* ── Road 2: carry the name alongside (the practical answer) ─────── */
struct named_op {
    const char *name;
    binop       fn;
};

static const struct named_op ops[] = {
    { "add", add }, { "sub", sub }, { "mul", mul },
};

static const char *name_of(binop f)
{
    for (size_t i = 0; i < sizeof ops / sizeof *ops; i++)
        if (ops[i].fn == f) return ops[i].name;      /* comparing function pointers is legal */
    return "(unknown function)";
}

int main(void)
{
    printf("sizeof(function pointer) = %zu, sizeof(void *) = %zu\n\n",
           sizeof(binop), sizeof(void *));

    puts("[Road 1] print the bytes — portable, but a riddle to a reader");
    char buf[2 + 2 * sizeof(binop) + 1];
    fmt_funcptr(buf, sizeof buf, add);
    printf("  length of add's address string: %zu characters (the value differs per run)\n",
           strlen(buf));
    printf("  does it start with \"0x\": %s\n", strncmp(buf, "0x", 2) == 0 ? "yes" : "no");

    puts("\n[Road 2] print the name — this is what a log should keep");
    binop chosen[] = { mul, add, sub };
    for (size_t i = 0; i < sizeof chosen / sizeof *chosen; i++)
        printf("  call %zu: %s(7, 3) = %d\n",
               i, name_of(chosen[i]), chosen[i](7, 3));

    puts("\n[comparing function pointers is inside the contract]");
    binop f = add, g = add, h = sub;
    printf("  equal when they point at the same function: %s\n", f == g ? "yes" : "no");
    printf("  different when the functions differ:      %s\n", f != h ? "yes" : "no");
    puts("  So \"which function is it\" can be answered without printing an address.");

    puts("\n[what not to do]");
    puts("  printf(\"%p\", f);          <- a function pointer to %p: outside the contract");
    puts("  printf(\"%p\", (void *)f);  <- outside ISO C (POSIX allows it). -Wpedantic warns");
    puts("  printf(\"%p\", (void *)&f); <- compiles, but that is the *variable's* address");
    return 0;
}

Output

sizeof(function pointer) = 8, sizeof(void *) = 8

[Road 1] print the bytes — portable, but a riddle to a reader
  length of add's address string: 18 characters (the value differs per run)
  does it start with "0x": yes

[Road 2] print the name — this is what a log should keep
  call 0: mul(7, 3) = 21
  call 1: add(7, 3) = 10
  call 2: sub(7, 3) = 4

[comparing function pointers is inside the contract]
  equal when they point at the same function: yes
  different when the functions differ:      yes
  So "which function is it" can be answered without printing an address.

[what not to do]
  printf("%p", f);          <- a function pointer to %p: outside the contract
  printf("%p", (void *)f);  <- outside ISO C (POSIX allows it). -Wpedantic warns
  printf("%p", (void *)&f); <- compiles, but that is the *variable's* address

59.4.1 Why it cannot be passed to %p

The reasons stack three deep. Peel them off one at a time.

First, %p takes a void * (or a character pointer) only (chapter 35). A function pointer is not on that list.

Second, a variadic function will not do the conversion for you. As chapter 58 showed, an argument passed through ... undergoes only the default argument promotions — integers to int, float to double. There is no such promotion for pointers. So the void * that %p requires has to be produced by an explicit cast at the call site. “It will sort itself out” does not apply here.

Third, and that cast is itself outside the standard, exactly as the previous section said. So the line below has no basis in the standard’s text, even where a compiler accepts it.

printf("%p", (void *)f);      /* a conversion ISO C does not define */

GCC’s own reaction, as checked for this book, says the same: turn on -Wpedantic and you get “ISO C forbids conversion of function pointer to object pointer type”.

★ The three layers in one line: %p demands a void *, a variadic call will not convert for you, and the conversion you write by hand is undefined. And the last link runs deepest — as the previous section showed, function pointers are a family of their own, separate from object pointers, so on a Harvard machine or an ABI built on function descriptors there is no guarantee that a void * can even hold a function address. It is not a problem of syntax but of the vessel.

Counter-example. Three common wrong answers

printf("%p", f);              /* 1: the function pointer itself — outside the contract */
printf("%p", (void *)f);      /* 2: a conversion outside ISO C (POSIX allows it) */
printf("%p", (void *)&f);     /* 3: compiles, warns about nothing, and… */

The third is the nastiest. &f is the address of the pointer variable, not of the function. The compiler says nothing, the output is a plausible hexadecimal number, and the value is entirely wrong. “No warning, so it must be right” does not hold here.

Platform note. POSIX fills the gap — dlsym, the real case

On Unix-like systems things differ. Because POSIX defines dlsym() as returning the address of a function as a void *, conversion between function pointers and void * has to work there. So in code aimed only at Linux, macOS and the BSDs, (void *)f is closer to a specification than to a habit.

★ But this filling has a price. What the specification requires is a conversion standard C does not define, and compilers still warn about it. So this idiom has settled in practice.

void (*fn)(void);
*(void **)&fn = dlsym(handle, "do_work");   /* the idiom that skirts the gap */

Look at what that line does — instead of converting, it overwrites the bytes of a function pointer variable as though they were an object pointer. The warning goes away and the contract is just as broken. “The standard leaves it undefined, the platform requires it anyway, and so people route around the standard one layer further” — a textbook grey area (chapter 12).

The discipline is unchanged: if you use it, write one line saying why it is safe here, and know what you would switch to when portability starts to matter. The question that follows is that way out.

Q. So is dlsym badly designed? If POSIX were being written today, how should it look?

A.Not so much badly designed as designed for a machine that no longer stands alone. When Unix gained dlopen/dlsym in the late 1980s, function addresses and data addresses on those machines were the same width in the same address space, and “an address is an address” was simply true. Returning everything through one void * was a frugal design.

It broke afterwards. ISO C fixed function pointers and object pointers as separate worlds, and machines where the two really differ — Harvard architectures, ABIs built on function descriptors — became common. dlsym’s signature turned into an API that demands a conversion the standard does not define. POSIX admits this itself; its APPLICATION USAGE says:

“ Note that conversion from a void * pointer to a function pointer … is not defined by the ISO C standard. This standard requires this conversion to work correctly on conforming implementations.1

★ “The standard does not define it, and we require it anyway.” A specification rarely writes that down. It is admitting to a patch.

So, today. The answer is known, and has even been implemented. Put the return type on the function-pointer side. FreeBSD did exactly that, under the name dlfunc().

dlfunc_t dlfunc(void *restrict handle, const char *restrict symbol);

The manual states it precisely — it “implements all of the behavior of dlsym(), but has a return type which can be cast to a function pointer without triggering compiler diagnostics.” And what that type is was left open on purpose at the specification level: “the precise return type of dlfunc() is unspecified; applications must cast it to an appropriate function pointer type.”2

★ What makes this inside the standard is §6.3.2.3: a function pointer converted to another function pointer type and back again compares equal to the original. So the caller casts back once, to its own signature, and that cast is a function pointer converted to a function pointer — inside the contract.

DesignReturnsRelation to the standard
dlsym (1980s)void *requires a conversion the standard leaves undefined
dlfunc (FreeBSD)dlfunc_t — unspecified function pointerfunction-to-function cast — inside the standard
Split in twodlsym_object / dlsym_functionmost honest, but the API grows

Table 60.1

Then why is it not the standard? Standards bodies do not add an interface without prior art, so FreeBSD implemented one to create that art. This is not guesswork but the record: the commit that added dlfunc, and one three hours later renaming the type from __dlfunc_t to dlfunc_t, which says it was “to match what I have proposed to the Austin Group.”3

★ The prior art never spread beyond FreeBSD, and dlfunc is still not in the specification. There is a bitter circularity here: it cannot become a standard because it did not spread, and nobody uses it because it is not a standard. What survives is not the better design but the one already in place.

★★ The lesson is not about dlsym but about API design generally. A decision that “these are the same on this machine, so let us merge them” became, thirty years later, a debt that forcibly bridges two concepts the standard keeps apart. It is the most expensive instance of what this book keeps repeating — a type is a contract, not a size (chapter 26).

Q. Then why not simply fix on one generic function pointer type that holds any function — say typedef void *(*generic_func_ptr_t)(void);?

A.The instinct is right, and it is exactly what dlfunc does. But it matters why it is right, and the spelling you choose makes a measurable difference.

First, why it works. The usual explanation is “function pointers are all the same size anyway” — ★ and that is not in the standard. §6.2.5 is fussy about which pointers it promises to be alike.

“ All pointers to structure types shall have the same representation and alignment requirements as each other. All pointers to union types shall have the same representation and alignment requirements as each other. Pointers to other types may not have the same representation or alignment requirements.

Function pointers fall under “other types”. So there is no promise about size.

The real basis is a different clause, §6.3.2.3 — and it is the stronger one.

“ A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined. ”

★ The difference matters. This is a promise about values, not about representations. The round trip holds even on a machine where the representations differ — which is why the design does not fall apart on a Harvard architecture. And note that the round trip for object pointers (§6.3.2.3 paragraph 7) carries a proviso about alignment, while the round trip for function pointers carries none.

In other words function pointers form a closed family, and inside that family any type will serve as the container. This book checked it directly: an int(*)(int,int), a double(*)(double) and a const char *(*)(void) were all stored in one generic type and taken back out; all three compared equal to the originals and called correctly afterwards.

So which spelling? Here the candidates part company. Three were measured.

spelling of the generic typeround tripif you forget the cast and just call it-Wcast-function-type
void (*)(void)worksit compiles — silent undefined behaviourno warning
void *(*)(void) (as proposed)worksit compiles — silent undefined behaviourwarns on every cast
void (*)(struct __dlfunc_arg)worksit does not compilewarns on every cast

Table 60.2

Two things come out of that.

One — there is no reason to make the return type void *. Whatever you write there, the round trip holds just the same, but writing void * carves a false claim into the type: that this function returns a pointer. And GCC recognizes only void (*)(void) as the generic function pointer, exempting it from -Wcast-function-type — the proposed spelling warns at every use. The idiomatic spelling is void (*)(void).

Two — and yet void (*)(void) is not perfect either, because it is a signature a real function might plausibly have. Forget to cast back, write f(), and the compiler says nothing; unless the real function really is void(void), that is undefined behaviour.

★ Which makes it worth looking at what FreeBSD actually chose, in the header itself.

struct __dlfunc_arg { int __dlfunc_dummy; };
typedef void (*dlfunc_t)(struct __dlfunc_arg);   /* FreeBSD <dlfcn.h> */

The parameter is a private structure nobody would really pass. This type is compatible with no real function, so the mistake of calling it directly cannot even be written — that is the last row of the table. Casting back is enforced not by good manners but by a compile error.

★★ To put it together: the idea of a generic function pointer is sound, and §6.3.2.3 is what holds it up. But a good generic type carries one more condition — it must not be callable on its own. A container should look like a container, not like something you can use without opening it.

59.4.2 The portable road — lift the bytes

To solve it with the standard alone, do not read the pointer as a value; move its bytes. memcpy is inside the contract for any type.

unsigned char raw[sizeof f];
memcpy(raw, &f, sizeof raw);
for (size_t i = sizeof raw; i-- > 0; ) printf("%02X", raw[i]);

The demonstration’s fmt_funcptr is that shape. What it gains and loses is plain — it compiles everywhere with no warning, and it is a riddle to a reader. On some platforms those bytes are not even the function’s entry point but the address of a descriptor (see the platform note below).

59.4.3 The best answer — a name instead of an address

The practical answer is the third road: do not print the address, print the name.

The demonstration’s struct named_op is that pattern. Keep a name string in the function table and the log holds a line a person reads at once, such as mul(7, 3) = 21. Comparing function pointers is guaranteed by the standard (equal when they point at the same function), so scanning the table for the name is inside the contract too.

MethodPortabilityValue as a log
Carrying the name alongside★ everywhere★ read directly by a person
Printing bytes with memcpy★ everywhereA riddle — needs symbols to decode
(void *)f through %pPOSIX onlyA riddle, as above
Recovering the name with dladdr, SymFromAddrPer platform★ Best when a name comes out

Table 60.3

In practice. Recovering a name from an address, and its limits

There is a way to go from an address to a name in a running program: dladdr() on Unix, DbgHelp’s SymFromAddr() on Windows, and the kernel’s %pS specifier seen earlier.

Running dladdr for this book showed the limits directly.

TargetResult
A static functionNo name found — it is not in the symbol table
An ordinary global functionNot found without -rdynamic; found with it
printfFound, but as the internal alias _IO_printf

Table 60.4

In other words, getting a name is a stroke of luck. A build that keeps no symbols — as release builds usually do — yields nothing, and what does come out may differ from the name in the source. So “turning an address back into a name” is a debugger’s job, and a log the program writes itself had better carry the name from the start.

Platform note. Machines where a function pointer is not one address

There is a reason this section follows the standard so carefully: platforms really existed where a function pointer was not a plain address.

  • On segmented x86, a far function pointer was a segment and an offset pair, differing from data pointers even in size.
  • On IBM AIX and the old Itanium ABI a function pointer pointed at a descriptor — a struct holding the entry point and a global data pointer. Print “the address” of two function pointers there and you get the addresses of those structs, not the entry points.
  • On Harvard-architecture microcontrollers, code and data live in different address spaces entirely.

That is why the standard never said “a function pointer can be converted to void *”, and never will. Looking only at an ordinary desktop it seems over-careful; go down to embedded and it is still alive today.

59.5 Dispatch tables — an array instead of a switch

The example’s ④ is that. Pair names with functions and lay them out in an array, and you choose by data instead of by branching. To add an item you mend only the table, not the code, and combined with chapter 57′s X macro you can even generate the table from a single list.

Grow this pattern and it becomes a state machine, a command interpreter, a plugin structure. And grow it further — that is the next section’s story.

59.6 The virtual function table — object orientation built in C

C has no classes. But put, as a struct’s first member, a pointer to a table of function pointers and you obtain polymorphism, the heart of object orientation.

examples-en/ch59/vtable.c

#include <stdio.h>
#include <string.h>

/* Object orientation built by hand in C: the table of virtual functions
   (a vtable) is kept apart from the data. The same skeleton GTK's GObject uses. */

struct shape;                       /* a forward declaration */

/* (1) the vtable — one per type, not one per instance */
struct shape_vtable {
    const char *name;
    double (*area)(const struct shape *self);
    void   (*describe)(const struct shape *self);
};

/* (2) the base "class": the first member of every object points at the table */
struct shape {
    const struct shape_vtable *vt;
};

/* (3) the derived "class": put the base first and the pointers convert to each other */
struct circle { struct shape base; double r; };
struct rect   { struct shape base; double w, h; };

static double circle_area(const struct shape *s)
{
    const struct circle *c = (const struct circle *)s;   /* being the first member, the address is the same */
    return 3.14159265358979 * c->r * c->r;
}
static double rect_area(const struct shape *s)
{
    const struct rect *r = (const struct rect *)s;
    return r->w * r->h;
}
static void generic_describe(const struct shape *s)
{
    printf("  %-9s area %.2f\n", s->vt->name, s->vt->area(s));
}

/* (4) the table is constant and one per type — an instance holds only a pointer */
static const struct shape_vtable circle_vt = { "circle",    circle_area, generic_describe };
static const struct shape_vtable rect_vt   = { "rectangle", rect_area,   generic_describe };

static struct circle make_circle(double r) { return (struct circle){ { &circle_vt }, r }; }
static struct rect   make_rect(double w, double h) { return (struct rect){ { &rect_vt }, w, h }; }

int main(void)
{
    struct circle c = make_circle(2.0);
    struct rect   r = make_rect(3.0, 4.0);

    /* (5) the same code handles different types — polymorphism */
    const struct shape *objs[] = { &c.base, &r.base };
    for (size_t i = 0; i < sizeof objs / sizeof objs[0]; i++)
        objs[i]->vt->describe(objs[i]);

    /* (6) the cost, seen with the eyes */
    printf("object sizes: circle=%zu, rect=%zu (the %zu-byte table pointer included)\n",
           sizeof(struct circle), sizeof(struct rect), sizeof(void *));
    printf("table size  : %zu (only one exists per type)\n", sizeof(struct shape_vtable));
    return 0;
}

Output

  circle    area 12.57
  rectangle area 12.00
object sizes: circle=16, rect=24 (the 8-byte table pointer included)
table size  : 24 (only one exists per type)

The design has four bones.

  1. One table per type (static const). Copying the function pointers into every instance makes objects large and the cache worse — so the table is kept separately in a single copy, and the object holds only one pointer to it.
  2. The base struct is the first member. The standard guarantees that “a struct’s first member begins at the same address as the struct itself”, so struct circle * and struct shape * can be safely gone between (chapter 46).
  3. Calls go through the tables->vt->area(s). This one line does exactly what a C++ virtual function call does.
  4. The object itself is passed as the first argument. Writing by hand the this that C++ hides.

The last two lines of the output show the cost. What grows per object is one pointer (8 bytes) only, and the table exists once per type.

In practice. GTK’s GObject — a hand-built object system in real use

The proof that this approach is not a toy is GTK. GTK, the major toolkit of the Linux desktop, is written in pure C and beneath it lies an object system called GObject. Its structure stands on the same bones we have just seen.

  • The first member of the instance struct points at the class struct (our vtable).
  • Function pointers are laid out in the class struct, and a derived class “overrides” some of them by writing its own functions over them.
  • Inheritance is expressed by putting the base struct as the first member, and type conversion is wrapped in macros with checks attached (things like GTK_WIDGET(x)).
  • On top of that ride reference counting, signals (the observer pattern) and a property system.

The same design can be seen elsewhere. The Linux kernel’s struct file_operations — a table holding the read and write functions that differ per file system — is exactly a vtable, and Windows’ COM is this very convention pinned down at the ABI level.

Q. Then what differs from C++‘s virtual functions?

A. The concept is the same; the degree of automation differs. In C++ the compiler makes the table, plants the pointer, connects it in the constructor, and checks type conversions. In C all of that is handwork, so there are many places to slip — an object whose table was not connected, a struct that broke the first-member rule, code that casts a derived type wrongly.

What is gained in exchange is clear too. Everything is visible — what is where. You can count how many tables there are, how many pointers are followed per call, how many bytes an object is. It is the place where this book’s constant refrain, “a language in which cost is visible”, appears just the same in object orientation.

One thing more. C++‘s virtual table layout is not settled by the standard (the ABI settles it). That is why, when mixing C and C++, class objects are not passed across the boundary and only extern "C" functions and plain structs are exchanged (chapter 96).

Recap

Function pointers in summary.

rulecontent
decaya function name used as a value becomes a pointer
f, &f, *fall the same pointer. &&f is a syntax error
call notationp(x) and (*p)(x) are identical
typereturn type + parameters. casting to another type and calling is outside the contract
void *no guarantee of conversion with function pointers (Harvard architecture)
dlsymPOSIX guarantees it separately. the *(void **)&fn idiom
dispatch tablechoosing by data instead of by branching
vtableone table per type, one pointer per object. the first-member rule is the ground
in the fleshGObject (GTK), the kernel’s file_operations, COM

Table 60.5

We are equipped even to handle functions as values. Yet several times in this chapter there were places where the declaration itself was hard to read and we fled to typedef — things like int (*(*s)(void))(int);. The next chapter pays that debt: C’s most notorious place, reading declarations, met head on with two ways of reading and with typedef.

Notes

  1. The Open Group Base Specifications, dlsym, APPLICATION USAGE. pubs.opengroup.org/…/dlsym.html
  2. FreeBSD dlfunc(3). man.freebsd.org/…dlfunc(3)
  3. Garrett Wollman, FreeBSD source commits dc12134a8 and fda230194, 2002-05-29. The first describes the new return type as one that can be cast to a function pointer “without turning your computer into a frog.” github.com/freebsd/freebsd-src