Casting to the Same-Sized Unsigned Type

Paul J. Lucas - Jul 10 - - Dev Community

Introduction

In cdecl, there’s this enumeration:

enum cdecl_show {
  CDECL_SHOW_PREDEFINED       = 1 << 0,
  CDECL_SHOW_USER_DEFINED     = 1 << 1,
  CDECL_SHOW_OPT_IGNORE_LANG  = 1 << 2
};
typedef enum cdecl_show cdecl_show_t;
Enter fullscreen mode Exit fullscreen mode

whose values are bit-flags that can be bitwise-or’d together.

What the flags do isn’t important here, but, briefly, they control which types are shown in response to a cdecl show command.

I was working on enhancing the behavior of the show command such that if no user-defined type having a specific name was shown, show a predefined type having the same name, if it exists, via code like:

if ( !showed_any && (show & CDECL_SHOW_USER_DEFINED) != 0 ) {
  show &= ~CDECL_SHOW_USER_DEFINED;
  show |= CDECL_SHOW_PREDEFINED;
  // ...
}
Enter fullscreen mode Exit fullscreen mode

That is, turn off the CDECL_SHOW_USER_DEFINED bit and turn on the CDECL_SHOW_PREDEFINED bit. The problem was, when compiling with the Wsign-conversion compiler option, I got:

show.c:244:8: warning: implicit conversion changes signedness: 'int' to 'unsigned int' [-Wsign-conversion]
  show &= ~CDECL_SHOW_USER_DEFINED;
       ~~ ^~~~~~~~~~~~~~~~~~~~~~~~
1 warning generated.
Enter fullscreen mode Exit fullscreen mode

This happens because enumeration values in C implicitly convert to their underlying type (here, int), but the ~ converts its operand to unsigned int.

The obvious way to silence the warning is to cast to unsigned first:

  show &= ~(unsigned)CDECL_SHOW_USER_DEFINED;  // no warning
Enter fullscreen mode Exit fullscreen mode

The problem is, in C prior to C23, you can’t be sure what the underlying type of an enumeration is. From the C11 standard §6.7.2.2 ¶4:

Each enumerated type shall be compatible with char, a signed integer type, or an unsigned integer type. The choice of type is implementation-defined.

If you don’t know what the underlying type is, in particular its size, you don’t know that unsigned int is the right choice of unsigned type because you want the sizes to match.

Given that cdecl_show has only the values 1, 2, and 4, it’s a pretty safe bet that its underlying type is int — but you can’t be sure. What’s needed is a way to cast to an unsigned type that’s the same size as the type of a given expression.

The Solution

Using _Generic and STATIC_IF (given in that article), we can implement:

#define TO_UNSIGNED_EXPR(N)               \
  STATIC_IF( sizeof(N) == sizeof(char),   \
    (unsigned char)(N),                   \
  STATIC_IF( sizeof(N) == sizeof(short),  \
    (unsigned short)(N),                  \
  STATIC_IF( sizeof(N) == sizeof(int),    \
    (unsigned int)(N),                    \
  STATIC_IF( sizeof(N) == sizeof(long),   \
    (unsigned long)(N),                   \
    (unsigned long long)(N) ) ) ) )
Enter fullscreen mode Exit fullscreen mode

where N is any numeric expression of any integral or enumeration type. The implementation is straight-forward: use a chain of STATIC_IFs to determine the size of the type of the expression N, then cast it to an unsigned type of the same size.

Given that, I can now write:

  show &= ~TO_UNSIGNED_EXPR( CDECL_SHOW_USER_DEFINED );
Enter fullscreen mode Exit fullscreen mode

and get no warning.

Conclusion

Once again, _Generic allows you to do some compile-time type introspection. The nice thing about TO_UNSIGNED_EXPR() is that it will always work even if the underlying type changes.

In addition to being useful for enumerations, it’s also useful for typedefs of integral types in that you’ll never have to look up what the underlying type of a typedef is to know which unsigned type to cast to.

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