3

Suppose we have the following well-known inheritance pattern in C with a parent, a derived child struct and a vtable:

struct parent;

struct vtable {
    void(*op)(struct parent *obj);
};

struct parent {
    struct vtable *vtable;
};

struct child {
    struct parent parent;
    int bar;
};

The vtable for struct child is then usually defined in the following way:

void child_op(struct parent *obj) {
    // Downcast
    struct child *child = (struct child *)obj;
    /* Do stuff */
}

static struct vtable child_vtable = {
    .op = child_op,
};

We can then use the vtable and have dynamic dispatch:

void foo(struct parent *obj) {
    obj->vtable->op(obj);
}

There are cases however, where we know the concrete type of a struct child, e.g. directly after the struct was allocated. In this case, it makes sense to skip the indirect function call through the vtable and call the specific implementation of op for struct child directly. To dos this however, we have to convert the pointer first. The “upcast” can be performed without actual casting.

void bar(void) {
    struct child *obj = malloc(sizeof(*obj));
    obj->parent.vtable = &child_vtable;
    // Need convert pointer to child into pointer to parent first
    child_op(&obj->parent);
}

Would it be legal however, to declare the child_op function taking a parameter of struct child * instead of the base type struct parent *, because we know, that it will be called only with pointers to struct child objects and cast the function pointer assigned to the vtable instead:

void child_op(struct child *obj) {
    // No need to cast here anymore
    /* Do stuff */
}

// Cast only when creating the vtable
// Is this undefined behaviour?
static struct vtable child_vtable = {
    .op = (void (*)(struct parent *))child_op,
};

void foo(struct child *obj) {
    // No need to do any conversion here anymore
    child_op(obj);
}

As one can see, this would save us from having to cast in child_op and converting struct child * into struct parent * in cases, where we are sure, we have a valid pointer to a struct child (and no pointer to a e.g. struct grandchild).

A similar issue is discussed here and I conclude from it, that it would be legal if the types void (*)(struct parent *) and void (*)(struct child *) are compatible.

It certainly works in practice with the compilers and platforms I tested. If it is indeed undefined behaviour, are there any cases, where this would actually cause problems and the missing cast would actually have done something? Are any well-known projects using this scheme?

EDIT: You can find the examples on godbolt:

Sebastian Schrader
  • 1,453
  • 15
  • 19
  • @chux: you are right, but surprisingly Clang accepts it... – Serge Ballesta Jan 10 '18 at 14:43
  • I see no place in the code in your question where you use the result of converting a function pointer. A converted pointer is stored in the `op` member of a `struct vtable`, but nothing reads that value, let alone uses it to call a function. Your `foo` function calls `child_op` directly, not through a pointer. Should we presume that `child_op` will be called through the pointer-to-an-incompatible-type stored in `op`? – Eric Postpischil Jan 10 '18 at 14:47
  • @SergeBallesta "warning: declaration does not declare anything [-Wmissing-declarations]", with `pedantic-errors` that doesn't compile. – Stargateur Jan 10 '18 at 14:49
  • An appropriate response to whether there are cases where this would cause problems is “If you lie to the compiler, it will get its revenge,” attributed to Henry Spencer. A supported way to accomplish the desired behavior in C is to declare the functions using the same type, a function taking a pointer to `void`. – Eric Postpischil Jan 10 '18 at 15:11
  • @EricPostpischil: but using a function taking a pointer to void will not save you from a downcast: the `void *` cannot be used without it. – Serge Ballesta Jan 10 '18 at 16:01

1 Answers1

1

Unfortunately, there is no valid way to reference a function with a struct child * parameter with a pointer to function with a struct parent * parameter. Of course we all know that it will work with any common implementation, because at assembly language level a pointer to function is the address of the function, and the casting of a pointer to struct, to a pointer to its first member is a no-op.

But precisely because it is a no-op, there is no reason to write UB code just to avoid it. So the correct way is to write the functions for the vtable with the exact declared signature and downcast the pointer to the correct pointed type as first instruction in the function. If the function has to exist with a child argument, just use a tiny wrapper in the vtable.


An alternative conformant method would be to use K&R style declarations for the vtable pointers also called empty parameter lists in n1570 draft for C11. 6.7.6.3 §15 says:

...If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions.

That means that if you declare:

struct vtable {
    void(*op)();              // empty parameter list in declaration
};

op can point to

void child_op(struct child *obj) {
    // No need to cast here anymore
    /* Do stuff */
}

and if you have declared child as:

struct child {
    struct parent parent;
    int bar;
};

Following code is legal C:

struct vtable child_vtable = { &child_op };
struct child foo = { { &child_vtable }, 25 };
foo.parent.vtable->op(&foo);

because as op is actually called with a struc child * parameter it is indeed compatible with child_op.

Unfortunately, this has 2 major drawbacks:

  • you definitely lose any compile time control on the number and types of the parameters (that's why prototypes were invented...)
  • it is explicetely an obsolescent feature:

6.11.6 Function declarators
1 The use of function declarators with empty parentheses (not prototype-format parameter type declarators) is an obsolescent feature.

TL/DR a downcast at the beginning of a function is not such a terrible thing, and it can save you from a future compiler revenge...

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • The Standard does not require that compilers behave in useful fashion if a non-prototyped function or function pointer is invoked with an argument type that does not match its first parameter. Given `void void f(struct child*p) {...} void (*fp)() = f; struct child *sp;`, calling `fp(&sp)` would have defined behavior, but calling `fp(&(sp.parent))` would not. – supercat Jan 11 '18 at 20:01
  • I'll opt to be on the safe then, rather than playing UB games with the compiler and resort to wrapper functions, which will perform the casts. – Sebastian Schrader Jan 13 '18 at 14:57