Casting to the Same-Sized Unsigned Type
Paul J. Lucas
Posted on July 10, 2024
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;
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;
// ...
}
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.
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
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) ) ) ) )
where N
is any numeric expression of any integral or enumeration type. The implementation is straight-forward: use a chain of STATIC_IF
s 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 );
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 typedef
s 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.
Posted on July 10, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024