Variable arguments of enum type

  • Thread starter Harald van Dijk
  • Start date
H

Harald van Dijk

Hello all,

Is it possible to know what an enumeration type promotes to? Given this
code,

#include <stdio.h>
#include <stdarg.h>

enum EnumerationType
{
Value1,
Value2,
Value3
};

void VarArgFunc(int dummy, ...)
{
va_list ap;
va_start(ap, dummy);
printf("VarArgFunc called with dummy=%d, ...=", dummy);
switch (va_arg(ap, /* ??? */))
{
case Value1: puts("Value1"); break;
case Value2: puts("Value2"); break;
case Value3: puts("Value3"); break;
}
va_end(ap);
}

int main(void)
{
enum EnumerationType variable = Value1;
VarArgFunc(0, variable);
}

what's the right type to pass to va_arg? I've been looking at code that
does just this, using va_arg(ap, enum EnumerationType), which fails
horribly at runtime when compiled on systems where enum EnumerationType
turns out to be compatible with unsigned char. What's the right way of
dealing with this? Will an enumeration type always promote to signed or
unsigned int, or can it also promote to something larger (both in theory
and in practise)?
 
B

Ben Pfaff

Harald van Dijk said:
Will an enumeration type always promote to signed or unsigned
int, or can it also promote to something larger (both in theory
and in practise)?

In theory, an implementation may choose any suitable integer
type:

Each enumerated type shall be compatible with char, a signed
integer type, or an unsigned integer type. The choice of type
is implementation-defined,108) but shall be capable of
representing the values of all the members of the
enumeration.

In practice, all the values of an enumeration must be in the
range of "int", so I would not expect a compiler to choose a type
wider than "int".

You could always remove all risk by transforming this code:

enum EnumerationType
{
Value1,
Value2,
Value3
};

into this:

enum
{
Value1,
Value2,
Value3
};
typedef int EnumerationType;

which is just about equivalent except that EnumerationType is
always int.
 
W

Walter Roberson

Harald van =?UTF-8?b?RMSzaw==?= said:
Is it possible to know what an enumeration type promotes to?

According to C89, each enumeration identifier is a constant of
type int . enumeration types are compatible with an integer type,
but the choice of integer type is implementation defined.

int main(void)
{
enum EnumerationType variable = Value1;
VarArgFunc(0, variable);
}


If you were to use

VarArgFunc(0, (unsigned int)variable);

then because the value of each identifier is of type int, you
know that if your variable 'variable' was set to one of the enumeration
identifiers that the cast will have a predictable result no matter
what the implementation type used for the enumeration type as a whole.

I used (unsigned int) instead of (int) against the off chance that
the implementation choose an unsigned integral type for the enumeration
type, in which case you could potentially run into issues with (int)
if the enumeration value currently exceeded INT_MAX. I can't think of
why an implementation would think it a good idea to use an unsigned
integral type to hold values that are allowed to be both negative and
positive (it'd need a lot of fixups to work right!) but the C89 verbage
doesn't prohibit it. I would imagine that -in practice-,
using (int) would be fine (and much easier to program with).
The C89 verbage is probably there to allow char or short to be
used if the declared constants fit.

Note that an enumeration type is not restricted to holding one
of the enumerated values -- anything between the minimum and maximum
of them is valid to store in the enumeration variable (but anything
outside that range might or might not fit.)
 
H

Harald van Dijk

In theory, an implementation may choose any suitable integer type:

Each enumerated type shall be compatible with char, a signed integer
type, or an unsigned integer type. The choice of type is
implementation-defined,108) but shall be capable of representing the
values of all the members of the enumeration.

In practice, all the values of an enumeration must be in the range of
"int", so I would not expect a compiler to choose a type wider than
"int".

That's a good point, thanks.
You could always remove all risk by transforming this code:

enum EnumerationType
{
Value1,
Value2,
Value3
};

into this:

enum
{
Value1,
Value2,
Value3
};
typedef int EnumerationType;

which is just about equivalent except that EnumerationType is always
int.

Unfortunately, this disables very useful compiler warnings. With

EnumerationType dummy;
switch (dummy)
{
case Value1: /*...*/ break;
case Value2: /*...*/ break;
}

I can get the compiler to warn me that I'm missing a case for Value3,
which is a useful enough warning that I don't want to disable it.
 
H

Harald van Dijk

If you were to use

VarArgFunc(0, (unsigned int)variable);

then because the value of each identifier is of type int, you know that
if your variable 'variable' was set to one of the enumeration
identifiers that the cast will have a predictable result no matter what
the implementation type used for the enumeration type as a whole.

I used (unsigned int) instead of (int) against the off chance that the
implementation choose an unsigned integral type for the enumeration
type, in which case you could potentially run into issues with (int) if
the enumeration value currently exceeded INT_MAX.

Luckily, I don't have to worry too much about that, there are plenty of
assert(enum_value >= min_enum_value && enum_value <= max_enum_value);
I can't think of why
an implementation would think it a good idea to use an unsigned integral
type to hold values that are allowed to be both negative and positive
(it'd need a lot of fixups to work right!) but the C89 verbage doesn't
prohibit it. I would imagine that -in practice-, using (int) would be
fine (and much easier to program with). The C89 verbage is probably
there to allow char or short to be used if the declared constants fit.

This I don't understand. Different enumeration types can have different
sizes. When a compiler sees

enum EnumType1 {
Value1 = 1, Value2 = 2
};

it knows that there aren't any negative values to worry about. When that
compiler later sees

enum EnumType2 {
Value1 = 1, Value2 = -1
};

enum EnumType2 can be distinct from enum EnumType1; there's no
requirement that all enumeration types are compatible. So why does the
compiler have to worry about enum EnumType1 holding negative values?
Note that an enumeration type is not restricted to holding one of the
enumerated values -- anything between the minimum and maximum of them is
valid to store in the enumeration variable (but anything outside that
range might or might not fit.)

Mostly true, and I've run into that already, with the assumption of real
code that 0x100 is a valid value for an enum which turned out to have a
range of 0x00 to 0xFF (unsigned char).

I say mostly true because

enum {
Value1 = 1,
Value2 = 2 };

must be capable of storing 3. The enum must be an integer type with at
least two value bits, and an integer type with at least two value bits
can _always_ store the value 3.
 
Y

ymuntyan

In theory, an implementation may choose any suitable integer
type:

Each enumerated type shall be compatible with char, a signed
integer type, or an unsigned integer type. The choice of type
is implementation-defined,108) but shall be capable of
representing the values of all the members of the
enumeration.

In practice, all the values of an enumeration must be in the
range of "int", so I would not expect a compiler to choose a type
wider than "int".

You could always remove all risk by transforming this code:

enum EnumerationType
{
Value1,
Value2,
Value3
};

into this:

enum
{
Value1,
Value2,
Value3
};
typedef int EnumerationType;

which is just about equivalent except that EnumerationType is
always int.

Why would you use an enum type in the first place then?
I mean, if you are using an enum *type*, then that's
because you want a separate type, no? If varargs stuff
forces you to do something unnatural, then it's better
to cast the function arguments to int instead, just as
in other cases where you got to cast varargs function
arguments.

By the way, I've seen libraries which assume that enum
types are always promoted to int or unsigned int, so it
must be so with all "popular" compilers.

Yevgen
 
B

Ben Pfaff

Why would you use an enum type in the first place then?

A number of reasons. Consider the alternatives: enums behave
better than #defines (they are scoped) and unlike "const int"
objects, they can be used in constant expressions. They are also
more convenient than either of those alternatives in that it is
easier to add, remove, and reorder elements.
I mean, if you are using an enum *type*, then that's because
you want a separate type, no?

There is very little separate about an enum type, since every
enum type is compatible with some integer type. The elements of
an enumeration type don't even have the type of that enumeration!
They can be freely mixed with integer types in expressions.
There's not much to lose by using "int" instead. Loss of useful
warnings in switch statements from a compiler is about the only
thing I can think of.
 
K

Kaz Kylheku

According to C89, each enumeration identifier is a constant of
type int . enumeration types are compatible with an integer type,
but the choice of integer type is implementation defined.

That sucks, because passing the constant as a trailing argument has
different semantics form passing an enum constant from the declaration
of that type. Doh!

Enum should have been properly designed from the start:

// proposed:
unsigned long enum foo { a, b, c };

foo is compatible with unsigned int, and a b c are constants of
unsigned int type.

This would actually be a backward-compatible extension, because the
combination of type specifiers is a constraint violation.

C++'s safer enums are not such a bad idea either, though orthogonal to
this problem. Safer enums would make the a, b, c constants themselves
of the enum type, and forbid implicit conversions from integral to
enum type. That could still be combined with the additional type
specifiers to request compatibility with a given integral type.

The restrict qualifier could be borrowed for enum for this purpose:

// proposed:
unsigned long restrict enum foo { a, b, c };

Now a, b, and c have ``enum foo'' type rather than unsigned long.
Moreover, assigments from arithmetic types to ``enum foo'' are
forbidden without a cast.

Since ``types other than pointer types ... shall not be restrict
qualified'', this is a backward-compatible extension.
 
Y

ymuntyan

A number of reasons. Consider the alternatives: enums behave
better than #defines (they are scoped) and unlike "const int"
objects, they can be used in constant expressions. They are also
more convenient than either of those alternatives in that it is
easier to add, remove, and reorder elements.

No, those are reasons to use enumerators themselves, not
the type. I.e. you get the very same benefits in
enum {A, B}; int a = A;
There is very little separate about an enum type, since every
enum type is compatible with some integer type.

Well, it's still a separate type, which allows gcc know
when to warn :)
The elements of
an enumeration type don't even have the type of that enumeration!
They can be freely mixed with integer types in expressions.

Even worse, code like the following will bite:

int a = -1;
enum {X, Y} b = X;
if (a > b) oops;
There's not much to lose by using "int" instead. Loss of useful
warnings in switch statements from a compiler is about the only
thing I can think of.

I guess it's psychological. I can easily imagine
how every enum type is replaced by int and nothing
changes, yet I find it very weird if something
supposed to hold only members of an enum (or values
made of those members) is not an object of that enum
type. Yet it's weird if someone uses an enum for
booleans instead of int. It's certainly psychological :)

Yevgen
 
C

CBFalconer

Harald said:
.... snip ...

This I don't understand. Different enumeration types can have
different sizes. When a compiler sees

enum EnumType1 {
Value1 = 1, Value2 = 2
};

it knows that there aren't any negative values to worry about.
When that compiler later sees

Not so. You can follow that with:

enum EnumType x, y, z;

x = -1; y = 1000; z = Value1;

and all is legitimate.
 
B

Ben Pfaff

No, those are reasons to use enumerators themselves, not
the type. I.e. you get the very same benefits in
enum {A, B}; int a = A;

I usually use enum types more for documentation than for anything
else. Seeing the enum type in the function prototype can be
enlightening sometimes.
 
H

Harald van Dijk

Harald van D?k wrote:
... snip ...

Not so. You can follow that with:

enum EnumType x, y, z;

x = -1; y = 1000; z = Value1;

and all is legitimate.

Yes, yet this shows nothing of interest. None of x, y, or z is going to
be set to a negative value on my system, and there is no conformance
problem in that regard. The first assignment will set x to 255. The
second assignment will set y to 232. The third assignment will set z to
1. All perfectly valid: since none of the enumeration constants are
negative, the compiler doesn't have to make the enumeration type support
negative values.
 
B

Ben Pfaff

Jack Klein said:
I have had a few situations where I had to port code that depended on
enumerated types being ints. One solution is:

enum EnumerationType
{
EnumerationType_Min = INT_MIN, /* include <limits.h>, of course */
Value1 = 0,
Value2,
Value3,
EnumerationType_Max = INT_MAX
};

This definitely forces the compiler to choose a type whose range
is at least as wide as int for the enumerated type. But the
compiler could choose short if short and int have the same range,
and DeathC could choose long or long long.
 
K

Keith Thompson

CBFalconer said:
Not so. You can follow that with:

enum EnumType x, y, z;

x = -1; y = 1000; z = Value1;

and all is legitimate.

Depends on what you mean by "legitimate".

Suppose enum EnumType is compatible with signed char, and suppose
signed char has a range of -128 to 127. Then the conversion of -1 or
1000 overflows, resulting in an implementation-defined value (or
possibly an implementation-defined signal in C99).
 
H

Harald van Dijk

Suppose enum EnumType is compatible with signed char, and suppose signed
char has a range of -128 to 127. Then the conversion of -1 or 1000
overflows, resulting in an implementation-defined value (or possibly an
implementation-defined signal in C99).

If enum EnumType is compatible with signed char, there's no problem
storing -1 in it. Were you thinking of unsigned char first, and did you
change it to signed char later?
 
B

Ben Pfaff

Keith Thompson said:
Depends on what you mean by "legitimate".

Suppose enum EnumType is compatible with signed char, and suppose
signed char has a range of -128 to 127. Then the conversion of -1 or
1000 overflows, resulting in an implementation-defined value (or
possibly an implementation-defined signal in C99).

-1 is in the range -128 to 127.
 
K

Keith Thompson

Harald van Dijk said:
If enum EnumType is compatible with signed char, there's no problem
storing -1 in it. Were you thinking of unsigned char first, and did you
change it to signed char later?

Whoops. Yeah, something like that.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,756
Messages
2,569,535
Members
45,008
Latest member
obedient dusk

Latest Threads

Top