3
\$\begingroup\$

I'm using enum in C to have constants for values that can be bitwise-or'd together, e.g.:

enum show {
  SHOW_PREDEFINED      = 1 << 0,
  SHOW_USER_DEFINED    = 1 << 1,
  SHOW_OPT_IGNORE_LANG = 1 << 2,
  // ...
};
typedef enum show show_t;

show_t s;
// ...

s &= ~SHOW_USER_DEFINED;  // "implicit conversion changes signedness"

If I'm compiling with -Wsign-conversion, I get:

foo.c:24:8: warning: implicit conversion changes signedness: 'int' to 'unsigned int' [-Wsign-conversion]
  s &= ~SHOW_USER_DEFINED;
    ~~ ^~~~~~~~~~~~~~~~~~
1 warning generated.

I can, of course, use a cast to silence the warning like:

s &= ~(unsigned)SHOW_USER_DEFINED;

But doing so requires that I know the appropriate unsigned integer type to cast to based on sizeof(show_t).

In C, enum constants are int by default, but if the largest possible value exceeds the largest possible signed int, then the implementation can choose an unsigned type having the same size (if all values are non-negative), or a larger signed type.

I want a way to cast to the smallest unsigned type that's sufficient to hold the largest non-negative value. I thought of:

#define INT_TO_UNSIGNED(N)                  \
  _Generic( ~(N),                           \
    char        : (unsigned char     )(N),  \
    signed char : (unsigned char     )(N),  \
    short       : (unsigned short    )(N),  \
    int         : (unsigned int      )(N),  \
    long        : (unsigned long     )(N),  \
    long long   : (unsigned long long)(N),  \
    default     : (N)                       \
  )

Then:

e &= ~INT_TO_UNSIGNED(SHOW_USER_DEFINED);  // no warning

How's that? Any potential problems? Or is there a better way to get what I want?

New contributor
Paul J. Lucas is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
7
  • 1
    \$\begingroup\$ The current question title, which states your concerns about the code, is too general to be useful here. Please edit to the site standard, which is for the title to simply state the task accomplished by the code. Please see How do I ask a good question?, as well as How to get the best value out of Code Review: Asking Questions for guidance on writing good question titles. \$\endgroup\$ Commented Jul 8 at 22:02
  • \$\begingroup\$ Cast them to the largest type. uintmax_t perhaps? Or use macros like Davislor suggests. Though I prefer C23's binary literals more. \$\endgroup\$
    – Harith
    Commented Jul 9 at 0:48
  • \$\begingroup\$ @Harith The point is specifically to cast them to an unsigned type that has the same size. \$\endgroup\$ Commented Jul 9 at 1:09
  • \$\begingroup\$ A _Generic implementation will not be portable, because a compiler is allowed to make enumeration constants any implementation-defined signed or unsigned integral type, and you don’t have a list of them or know which are aliases for each other. For example, there are compilers where int and even short are 64-bit, but they are still allowed to represent an enum as a 32-bit quantity. C11 allows int32_t to be an alias for long, int, short or char, distinct from any of those, or not to exist at all. \$\endgroup\$
    – Davislor
    Commented Jul 9 at 7:53
  • \$\begingroup\$ @Davislor But I do have a list of them: they're the standard built-in types. All I really care about is their sizes. See my second potential solution below using STATIC_IF(). \$\endgroup\$ Commented Jul 9 at 12:38

3 Answers 3

4
\$\begingroup\$

This happens because the constants of an enum have (correction:) some implementation-defined integral type, which might be unsigned or signed, and there is no standard way to retrieve it. Real-world compilers would choose a type no wider than int here, however. Before C23, you could instead write the constants as macros:

#define SHOW_PREDEFINED      0x1U
#define SHOW_USER_DEFINED    0x2U
#define SHOW_OPT_IGNORE_LANG 0x4U

However, this gives up the compiler being able to tell that the case statements of a switch are exhaustive.

In C23, you can specify an enumeration member type:

enum show : unsigned int {

In C11, the uintmax_t type is at least as wide as any enumeration constant, so a conversion to that will not implicitly become unsigned or get truncated, and is at worst not optimal. (This is not always true in C23, where an implementation might have integral types wider than uintmax_t.)

You could also disable that specific warning flag, if there’s no way around it. Many compilers have a #pragma for it.

\$\endgroup\$
4
  • \$\begingroup\$ +1. And also gives up the names of the constants under a debugger. \$\endgroup\$
    – Harith
    Commented Jul 9 at 0:50
  • \$\begingroup\$ As I stated in my question, no, the constants of enum are not necessarily of type int. If you have 32-bits ints and have an enum constant with a value of 2147483648, its type will not be int. I should have stated: yes, I'm aware of C23; no, I can't use C23. \$\endgroup\$ Commented Jul 9 at 1:07
  • \$\begingroup\$ @Davislor 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." †128: "An implementation may delay the choice of which integer type until all enumeration constants have been seen." \$\endgroup\$ Commented Jul 9 at 1:24
  • \$\begingroup\$ @PaulJ.Lucas So it is. \$\endgroup\$
    – Davislor
    Commented Jul 9 at 1:44
1
\$\begingroup\$

Here's another stab at it. Given STATIC_IF() (see here for details):

#define STATIC_IF(EXPR,THEN,ELSE)     \
  _Generic( &(char[1 + !!(EXPR)]){0}, \
    char (*)[2]: (THEN),              \
    char (*)[1]: (ELSE)               \
  )

then:

#define TO_UNSIGNED_V3(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) ) ) ) )

I believe this works even if some types have the same sizes as others, e.g., if sizeof(long) == sizeof(long long)

I think it's possible to combine the two concepts into a single, less verbose macro:

#define TO_UNSIGNED_V4(N)           \
  _Generic( &(char[sizeof(N)]){0},  \
    char (*)[1]: (uint8_t )(N),     \
    char (*)[2]: (uint16_t)(N),     \
    char (*)[4]: (uint32_t)(N),     \
    char (*)[8]: (uint64_t)(N)      \
  )
New contributor
Paul J. Lucas is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
3
  • \$\begingroup\$ The first case doesn’t work if there is no 32-bit standard type. For the second one, why take sizeof at all when you could just make the cases int8_t: (uint8_t)(N) and so on? Nicer and doesn’t make any assumptions about CHAR_BIT. It could conceivably break on the DeathStation 9000 with the -int-bits=24 flag. \$\endgroup\$
    – Davislor
    Commented Jul 9 at 23:19
  • \$\begingroup\$ @Davislor When you say "the first case," do you mean TO_UNSIGNED_V3? Can't use int8_t, etc., on the left-hand side in _Generic because then no enum type will match since every enum is a distinct type. You need to use sizeof. I want an unsigned type whose size matches any signed integral or enum type. I suppose instead of uint8_t, etc., I could use uint_least8_t, etc. Even though TO_UNSIGNED_V3 is more verbose, it's better in that it doesn't rely on the uint8_t, etc., types. \$\endgroup\$ Commented Jul 9 at 23:34
  • \$\begingroup\$ Ah, OK. That looks like it works on mainstream architectures. I would probably just add the casts manually. \$\endgroup\$
    – Davislor
    Commented Jul 10 at 2:50
0
\$\begingroup\$

My original proposal using _Generic was over-engineered. A much simpler solution seems to be:

#define TO_UNSIGNED_V2(N)  ((N) + 0u)

The 0u is an unsigned literal. Adding zero doesn't change the value, but does coerce the type to be unsigned since "the usual integer promotions" in C promote signed to unsigned (of the same size) if one operand is unsigned.

New contributor
Paul J. Lucas is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
3
  • 2
    \$\begingroup\$ This doesn’t work portably for a signed type wider than int. The usual arithmetic conversions in C11 say (§6.3.1.8 ¶1) “Otherwise, if the type of the operand with signed integer type can represent all of the values of the type of the operand with unsigned integer type, then the operand with unsigned integer type is converted to the type of the operand with signed integer type. ” Therefore, 1LL + 0U has type signed long long int, if long long int is wider than int. \$\endgroup\$
    – Davislor
    Commented Jul 9 at 1:52
  • \$\begingroup\$ @Davislor You're right. D'oh! \$\endgroup\$ Commented Jul 9 at 2:02
  • 4
    \$\begingroup\$ Converting to (uintmax_t) should at least get you a safe conversion to an unsigned type. Before C23, it must be at least wide enough to hold any enumeration constant. At worst, it won't be optimal. \$\endgroup\$
    – Davislor
    Commented Jul 9 at 2:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.