C++ New Style Casts in C (sort of)

Paul J. Lucas - Oct 16 '22 - - Dev Community

Introduction

Occasionally, C++ features, such as function prototypes and // comments, get “back-ported” to C. Even though C++ has had “new style” casts (const_cast, static_cast, dynamic_cast, and reinterpret_cast) for 30 years, these have yet to be back-ported (if ever). While the motivation for new style casts is detailed in Bjarne’s paper, it boils down to:

  1. Make the intent of the program author clear to the reader.
  2. Enable (or suppress) more compiler warnings.

Is there any way to simulate new style casts in C? Sort of.

A First Cut

At the very least, we can define the following:

#define CONST_CAST(T,EXPR)        ((T)(EXPR))
#define STATIC_CAST(T,EXPR)       ((T)(EXPR))
#define REINTERPRET_CAST(T,EXPR)  ((T)(EXPR))
Enter fullscreen mode Exit fullscreen mode

(Noticeably absent is DYNAMIC_CAST. C doesn’t support either struct inheritance or virtual functions, so a C style cast of (T)expr can never mean “dynamic cast” in C, hence its omission.)

Having such macros makes the intent of the program author clear to the reader. For example, given:

*p++ = (char*)opt;
Enter fullscreen mode Exit fullscreen mode

was the intent to change the type of opt? Cast away const? Both? We can’t tell. However, using a macro:

*p++ = CONST_CAST(char*, opt);
Enter fullscreen mode Exit fullscreen mode

makes the intent clear. The same would be true when using the other cast macros. Clearer though the macros are, they’re still just syntactic sugar. Can they be made to be more than that? Some of them can — a little.

Improving the Macros

It turns out, unfortunately, that CONST_CAST and STATIC_CAST can’t be improved. However, since the former is used only for casting away const and the latter is used only for “well-behaved” casts (like casting an int to a double), it’s not that bad. However, REINTERPRET_CAST can be improved — a little.

Suppose there’s an API you’re using that allows “user data” to be given. In C, this is usually done by passing your data to a void* parameter, for example when visiting the nodes of a red-black tree. Assume we have a tree where the data we store at each node is:

struct my_struct {
    // ...
    int value;
};
Enter fullscreen mode Exit fullscreen mode

To find the first node having a given value:

rb_node_t* find_value( rb_tree_t const *tree, int value ) {
    return rb_tree_visit(
        tree, &find_val_visitor,
        REINTERPRET_CAST(void*, value)
    );
}
Enter fullscreen mode Exit fullscreen mode

where we cast value to void* to pass it for use as the data to be passed to the visitor function for each node visited:

bool find_val_visitor( void *node_data, void *v_data ) {
    struct my_struct const *const ms = node_data;
    int const to_find = REINTERPRET_CAST(int, v_data);
    return ms->value == to_find;
}
Enter fullscreen mode Exit fullscreen mode

In the visitor, we have to cast node_data* to my_struct* and v_data (the visitor data) back to an int. The problem with the second cast is that gcc will give a warning (typically on 64-bit platforms):

prog.c: 42:52: warning: cast to smaller integer type 'int'
from 'void *' [-Wpointer-to-int-cast]
Enter fullscreen mode Exit fullscreen mode

Normally, such a warning is a help; however, in this case, we really mean to do the cast. Is there any way to suppress the warning for only this case? If we redefined REINTERPRET_CAST like this:

#define REINTERPRET_CAST(T,EXPR)  ((T)(uintmax_t)(EXPR))
Enter fullscreen mode Exit fullscreen mode

then the warning goes away. Why? Because casting void* first to uintmax_t casts it to an integer type at least as large as sizeof(void*); then casting that to int generates no warning.

However, on a 32-bit platform where sizeof(void*) is 4 (yes, such platforms still exist), then we’d get the opposite warning when casting the int to void*:

prog.c 42:52: warning cast to pointer from integer of
different size [-Wint-to-pointer-cast]
Enter fullscreen mode Exit fullscreen mode

The unfortunate conclusion is that there’s no way to define a single REINTERPRET_CAST macro that works for both integer/pointer casting and casting between two non-pointer types. Instead, we can define two macros:

#define INTEGER_CAST(T,EXPR)      ((T)(uintmax_t)(EXPR))
#define POINTER_CAST(T,EXPR)      ((T)(uintptr_t)(EXPR))
Enter fullscreen mode Exit fullscreen mode

where INTEGER_CAST is used to cast to or from an integer type from or to an opaque type (more later) and POINTER_CAST is used to cast to or from a pointer type. Rewriting the code to use POINTER_CAST:

bool find_val_visitor( void *node_data, void *v_data ) {
    struct my_struct const *const ms = node_data;
    int const to_find = POINTER_CAST(int, v_data);
    return ms->value == to_find;
}

rb_node_t* find_value( rb_tree_t const *tree, int value ) {
    return rb_tree_visit(
        tree, &find_val_visitor,
        POINTER_CAST(void*, value)
    );
}
Enter fullscreen mode Exit fullscreen mode

makes the intent clear.

INTEGER_CAST

What, exactly, is the use-case for INTEGER_CAST? Suppose you have an API that defines an “opaque” type. Opaque types are used where the implementation hides the exact type because it may vary by platform or may change in the future:

typedef /* implementation defined */ api_user_data_t;
Enter fullscreen mode Exit fullscreen mode

Despite that, you’re still supposed to write “user data” into it and read it back out again.

The red-black tree API presented earlier might want to allow you to pass the largest possible data around for visitor data. Using void* on a 32-bit platform halves the size of such data from the more common 64-bit platforms. To guarantee 64 bits regardless of platform, it might define the following instead of using void*:

typedef uintmax_t visit_data_t;
Enter fullscreen mode Exit fullscreen mode

However, we (as the API users) aren’t supposed to know (or care) about the underlying type. We’d then rewrite our code using the opaque type:

bool find_val_visitor( void *node_data, visit_data_t v_data ) {
    struct my_struct const *const ms = node_data;
    int const to_find = INTEGER_CAST(int, v_data);
    return ms->value == to_find;
}

rb_node_t* find_value( rb_tree_t const *tree, int value ) {
    return rb_tree_visit(
        tree, &find_val_visitor,
        INTEGER_CAST(visit_data_t, value)
    );
}
Enter fullscreen mode Exit fullscreen mode

Since we’re now casting to or from an integer type from or to an opaque type — where we don’t know whether it’s a pointer or not — we must use INTEGER_CAST to guarantee it’ll work with the largest possible integer.

Conclusion

Use the right cast macro in the right circumstance:

  • CONST_CAST only for casting away const.
  • STATIC_CAST for “well-behaved” casts.
  • POINTER_CAST for casting to or from a pointer type.
  • INTEGER_CAST for casting to or from an integer from or to an opaque type.

Using these cast macros makes the intent of the program author clear to the reader. Use of POINTER_CAST (or INTEGER_CAST), unlike STATIC_CAST, additionally alerts the reader that a not-so-well-behaved cast is being performed and so should merit more scrutiny.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .