va_arg and short

  • Thread starter Glen Herrmannsfeldt
  • Start date
G

Glen Herrmannsfeldt

I was compiling a program written by someone else about six years ago, and
widely distributed at the time. It also includes makefiles for many
different systems, so I know it has been compiled with many different
compilers.

I got compile errors when it used va_arg to fetch an argument of type short.
That seemed a little strange to me, so I changed it to int and it compiled
just fine.

So now I wonder, just what is the rule for va_arg and short? It would seem
strange to reject certain types, yet that is what the compiler did.

-- glen
 
C

Chris Torek

I got compile errors when [some code] used va_arg to fetch an
argument of type short. That seemed a little strange to me, so
I changed it to int and it compiled just fine.

So now I wonder, just what is the rule for va_arg and short?

It is up to the programmer to avoid passing "narrow" types to the
va_arg() macro.

The type name given to va_arg() must match the type of the actual
parameter, whatever it was. In a call to a function that lacks a
prototype, or whose prototype ends in ", ..." (where the actual
argument matches the ", ..." part), the compiler is obligated to
perform the "default argument promotions". These replace (signed)
char and short with int, and float with double. They also replace
unsigned char and unsigned short with some wider type, but the
wider type is not easily predicted because the ANSI X3J11 committee
folks chose the wrong rules in 1985 or so. (Plain char will widen
to signed int on any sensible platform, but even this is not
guaranteed.)

Hence, for strict conformance, one might code something like this:

case 'i': /* obtain an int */
int_val = va_arg(ap, int);
...
break;

case 's': /* obtain a (signed) short */
short_val = (short)va_arg(ap, int);
...
break;

case 'S': /* obtain an unsigned short */
#if USHRT_MAX > INT_MAX
ushort_val = (unsigned short)va_arg(ap, unsigned int);
#else
ushort_val = (unsigned short)va_arg(ap, int);
#endif
...
break;

Note the icky need for a "#if" test, to tell which type unsigned
short becomes under the default argument promotions. (Had C used
the "predictably surprising" rule "unsigned promotes to unsigned",
the #if would not be required. Of course, this rule would also
have to go with one that says that if plain char is unsigned, it
widens to signed int anyway, possibly with a secondary requirement
for UCHAR_MAX to be no greater than INT_MAX on any such system.
This would not have been a hardship for existing implementations,
and the resulting rules are far easier to work with and do not
change from one implementation to another the way the current ones
do.)
It would seem strange to reject certain types, yet that is what
the compiler did.

A C compiler is not required to detect and reject such usage -- it
falls under the broad umbrella of undefined behavior -- but a kind
and clever compiler will do it. :)
 
J

James Hu

I was compiling a program written by someone else about six years ago, and
widely distributed at the time. It also includes makefiles for many
different systems, so I know it has been compiled with many different
compilers.

I got compile errors when it used va_arg to fetch an argument of type short.
That seemed a little strange to me, so I changed it to int and it compiled
just fine.

So now I wonder, just what is the rule for va_arg and short? It would seem
strange to reject certain types, yet that is what the compiler did.

C99: 6.3.1.1, paragraph 2

"If an int can represent all the values of the original type, the
value is converted to an int; otherwise, it is converted to an
unsigned int. These are called the /integer promotions/."

C99: 6.5.2.2, paragraph 6

"If the expression that denotes the called function has a type that
does not include a prototype, the integer promotions are performed
on each argument, and arguments that have type float are promoted
to double. These are called the /default argument promotions/.

C99: 6.5.2.2, paragraph 7

"The ellipses notation in a function prototype declarator causes
argument type conversion to stop after the last declared parameter.
The default argument promotions are performed on trailing arguments."


-- James
 
G

Glen Herrmannsfeldt

Chris Torek said:
I got compile errors when [some code] used va_arg to fetch an
argument of type short. That seemed a little strange to me, so
I changed it to int and it compiled just fine.

So now I wonder, just what is the rule for va_arg and short?

It is up to the programmer to avoid passing "narrow" types to the
va_arg() macro.

The type name given to va_arg() must match the type of the actual
parameter, whatever it was. In a call to a function that lacks a
prototype, or whose prototype ends in ", ..." (where the actual
argument matches the ", ..." part), the compiler is obligated to
perform the "default argument promotions". These replace (signed)
char and short with int, and float with double. They also replace
unsigned char and unsigned short with some wider type, but the
wider type is not easily predicted because the ANSI X3J11 committee
folks chose the wrong rules in 1985 or so. (Plain char will widen
to signed int on any sensible platform, but even this is not
guaranteed.)

I knew about the default promotions, so it wasn't hard to know what to
change it to. The program was so widely distributed, and run on so many
different compilers, I was surprised that it hadn't been seen before. Now,
most likely it is never executed. It is a modified version of sprintf(),
and if the h flag is used it will print a short. I don't know anyone that
ever did that, though they may have been surprised if they did.

(snip)
A C compiler is not required to detect and reject such usage -- it
falls under the broad umbrella of undefined behavior -- but a kind
and clever compiler will do it. :)

Apparently compilers have gotten kinder and cleverer in recent years.

-- glen
 
C

Christian Bau

"Glen Herrmannsfeldt said:
I was compiling a program written by someone else about six years ago, and
widely distributed at the time. It also includes makefiles for many
different systems, so I know it has been compiled with many different
compilers.

I got compile errors when it used va_arg to fetch an argument of type short.
That seemed a little strange to me, so I changed it to int and it compiled
just fine.

So now I wonder, just what is the rule for va_arg and short? It would seem
strange to reject certain types, yet that is what the compiler did.

When you use the va_arg macro, the type that you supply must (with some
exceptions that don't apply here) match the type of the actual argument
_after default promotions_.

Any argument of type short would be promoted to int, so using short in
the va_arg macro can never match the type of the actual argument after
default promotions.
 
P

Peter Nilsson

[Cross posted to comp.std.c.]

Glen Herrmannsfeldt said:
I was compiling a program written by someone else about six years ago, and
widely distributed at the time. It also includes makefiles for many
different systems, so I know it has been compiled with many different
compilers.

I got compile errors when it used va_arg to fetch an argument of type short.
That seemed a little strange to me, so I changed it to int and it compiled
just fine.

So now I wonder, just what is the rule for va_arg and short? It would seem
strange to reject certain types, yet that is what the compiler did.

I wonder why va_arg could not accept a type subject to promotion and
handle them accordingly, i.e. accept the promoted type implicitly and
convert it to the original/requested type. [So, cases like the
character types and unsigned short, which might promote to signed or
unsigned int, could be handled without having to resort to integer
limit checks.]

What sorts of architectures would make such an alternative va_arg
behaviour difficult to implement in practice?

[I appreciate the status quo is not likely to change, I'm just curious
as to whether it was considered for C90 or C99, or if not, how the
Committee might discus the request.]
 
G

Gordon Burditt

I wonder why va_arg could not accept a type subject to promotion and
handle them accordingly, i.e. accept the promoted type implicitly and
convert it to the original/requested type. [So, cases like the
character types and unsigned short, which might promote to signed or
unsigned int, could be handled without having to resort to integer
limit checks.]

What sorts of architectures would make such an alternative va_arg
behaviour difficult to implement in practice?

If C had type-valued functions, this might be easier to deal with.
For example,
type_standard_promotions(typeof(short)) == typeof(int)
I don't think clever tricks with macros can deal with this without
also using a magic compiler built-in.

As it stands, the second argument of va_arg has to be a valid type
if you append a "*" to it; this restriction wouldn't be necessary
if you had:
type_addressof(typeof(int)) == typeof(int *)
[I appreciate the status quo is not likely to change, I'm just curious
as to whether it was considered for C90 or C99, or if not, how the
Committee might discus the request.]

Gordon L. Burditt
 
C

Christian Bau

[Cross posted to comp.std.c.]

Glen Herrmannsfeldt said:
I was compiling a program written by someone else about six years ago, and
widely distributed at the time. It also includes makefiles for many
different systems, so I know it has been compiled with many different
compilers.

I got compile errors when it used va_arg to fetch an argument of type
short.
That seemed a little strange to me, so I changed it to int and it compiled
just fine.

So now I wonder, just what is the rule for va_arg and short? It would
seem
strange to reject certain types, yet that is what the compiler did.

I wonder why va_arg could not accept a type subject to promotion and
handle them accordingly, i.e. accept the promoted type implicitly and
convert it to the original/requested type. [So, cases like the
character types and unsigned short, which might promote to signed or
unsigned int, could be handled without having to resort to integer
limit checks.]

These are no problem anyway, because the C Standard explicitely allows
you to use for example unsigned int instead of int if the value passed
can be represented in both types. So whether unsigned short is passed as
int or unsigned int, you can always read it as an unsigned int.
What sorts of architectures would make such an alternative va_arg
behaviour difficult to implement in practice?

Many implementations implement va_arg as a macro, without requiring any
special knowledge in the compiler itself. Producing code that works for
"short" if sizeof (short) < sizeof (int) by using a macro alone is
difficult. So this requirement would mean you have to change the
compiler.

On the other hand, as a compiler writer I would say that the best
possible result is a compile time error. That way the programmer has a
chance to change their code. Much better than accepting code that will
be accepted and lead to a crash on the next machine.
 
J

James Kuyper

[Cross posted to comp.std.c.]

Glen Herrmannsfeldt said:
I was compiling a program written by someone else about six years ago, and
widely distributed at the time. It also includes makefiles for many
different systems, so I know it has been compiled with many different
compilers.

I got compile errors when it used va_arg to fetch an argument of type short.
That seemed a little strange to me, so I changed it to int and it compiled
just fine.

So now I wonder, just what is the rule for va_arg and short? It would seem
strange to reject certain types, yet that is what the compiler did.

I wonder why va_arg could not accept a type subject to promotion and
handle them accordingly, i.e. accept the promoted type implicitly and
convert it to the original/requested type. [So, cases like the
character types and unsigned short, which might promote to signed or
unsigned int, could be handled without having to resort to integer
limit checks.]

What sorts of architectures would make such an alternative va_arg
behaviour difficult to implement in practice?

The normal va_arg() macro can, on many platforms, expand to normal C
code. On some platforms, something like the following is sufficient:

typedef char* va_list;
#define va_start(arg) )(&arg+sizeof(arg))
#define va_arg(ap,type) ((type*)(ap+=sizeof(type)))[-1]
#define va_end(ap)

Try to figure out how to modify that without using any extensions to C
such as sizeof(promoted_typeof(type)). An implementation is free, of
course, to use extensions, but I think that the committee would resist
changing the specification in a way that would make the use of an
extension mandatory.
 
T

Thad Smith

Peter said:
I wonder why va_arg could not accept a type subject to promotion and
handle them accordingly, i.e. accept the promoted type implicitly and
convert it to the original/requested type. [So, cases like the
character types and unsigned short, which might promote to signed or
unsigned int, could be handled without having to resort to integer
limit checks.]

While the programmer should know that the type char and short are
promoted, what about int32_t? Can it be an argument of va_arg? It
might be equivalent to a short (needing promotion), or a int or a long.
Is there any way of portably accessing a variadic int32_t (or intX_t for
X >= 16) parameter?

Thad
 
D

Dan Pop

In said:
I was compiling a program written by someone else about six years ago, and
widely distributed at the time. It also includes makefiles for many
different systems, so I know it has been compiled with many different
compilers.

I got compile errors when it used va_arg to fetch an argument of type short.
That seemed a little strange to me, so I changed it to int and it compiled
just fine.

So now I wonder, just what is the rule for va_arg and short? It would seem
strange to reject certain types, yet that is what the compiler did.

It's a clear case of undefined behaviour:

The va_arg macro expands to an expression that has the type and
value of the next argument in the call. The parameter ap shall be the
same as the va_list ap initialized by va_start. Each invocation of
va_arg modifies ap so that the values of successive arguments are
returned in turn. The parameter type is a type name specified such
that the type of a pointer to an object that has the specified type
can be obtained simply by postfixing a * to type. If there is no
actual next argument, or if type is not compatible with the type of
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
the actual next argument (as promoted according to the default
^^^^^^^^^^^^^^^^^^^^^^^^^=====================================
argument promotions), the behavior is undefined.
====================^^^^^^^^^^^^^^^^^^^^^^^^^^^

A short argument is promoted to int, therefore, by specifying short as the
type you're guaranteed to have a type mismatch, leading to undefined
behaviour. The compiler that rejected it just did you a favour. The code
was written by an incompetent programmer.

Dan
 
G

Glen Herrmannsfeldt

Dan Pop said:
In <aE0pb.81932$HS4.680953@attbi_s01> "Glen Herrmannsfeldt"
(snip)

A short argument is promoted to int, therefore, by specifying short as the
type you're guaranteed to have a type mismatch, leading to undefined
behaviour. The compiler that rejected it just did you a favour. The code
was written by an incompetent programmer.

I don't disagree with that. This program is part of a widely distributed
program library, and I am sure has been compiled on many compilers over the
years. Most likely, this statement has never been executed, so it really
doesn't matter what it compiles into. The problem comes when a program is
supposed to be portable to a wide variety of machines and compilers, with
the expectation that it can be compiled and installed by non-C programmers.
Someone is supposed to be able to untar it, run make, and not have any
compiler or link errors. Considering that it passed so many compilers on
the way, would it have been reasonable to make it a warning?

The next error I ran into, in a different library, came from not finding the
include file ndbm.h. It seems that it is now db.h, at least on the system
that I was using.

Oh well.

-- glen
 
P

Peter Nilsson

[Cross posted to comp.std.c.]

Glen Herrmannsfeldt said:
I was compiling a program written by someone else about six years ago, and
widely distributed at the time. It also includes makefiles for many
different systems, so I know it has been compiled with many different
compilers.

I got compile errors when it used va_arg to fetch an argument of type
short. That seemed a little strange to me, so I changed it to int and it
compiled just fine.

So now I wonder, just what is the rule for va_arg and short? It would
seem strange to reject certain types, yet that is what the compiler did.

I wonder why va_arg could not accept a type subject to promotion and
handle them accordingly, i.e. accept the promoted type implicitly and
convert it to the original/requested type. [So, cases like the
character types and unsigned short, which might promote to signed or
unsigned int, could be handled without having to resort to integer
limit checks.]

What sorts of architectures would make such an alternative va_arg
behaviour difficult to implement in practice?

The normal va_arg() macro can, on many platforms, expand to normal C
code. On some platforms, something like the following is sufficient:

typedef char* va_list;
#define va_start(arg) )(&arg+sizeof(arg))

#define va_start(ap,arg) ((ap) = (char *) &arg) /* ? */
#define va_arg(ap,type) ((type*)(ap+=sizeof(type)))[-1]
#define va_end(ap)

Try to figure out how to modify that without using any extensions to C
such as sizeof(promoted_typeof(type)). An implementation is free, of
course, to use extensions, but I think that the committee would resist
changing the specification in a way that would make the use of an
extension mandatory.

The 'extension' is static type analysis, indeed, the *same* analysis
used to promote parameters in variadic function calls in the first
place.
 
P

Peter Nilsson

Thad Smith said:
Peter said:
I wonder why va_arg could not accept a type subject to promotion and
handle them accordingly, i.e. accept the promoted type implicitly and
convert it to the original/requested type. [So, cases like the
character types and unsigned short, which might promote to signed or
unsigned int, could be handled without having to resort to integer
limit checks.]

While the programmer should know that the type char and short are
promoted, what about int32_t? Can it be an argument of va_arg? It
might be equivalent to a short (needing promotion), or a int or a long.
Is there any way of portably accessing a variadic int32_t (or intX_t for
X >= 16) parameter?

A tedious one...

#if INT32_MAX <= INT_MAX
int32_t x = va_arg(ap, int);
#else
int32_t x = va_arg(ap, int32_t);
#endif
 
C

Chris Torek

#define va_start(ap,arg) ((ap) = (char *) &arg) /* ? */

No -- the idea is to take the address of the "variable" named "..."
in something like:

void f(const char *fmt, ...) {
va_list ap;

va_start(ap, fmt);
/* code here */
va_end(ap);
}
#define va_arg(ap,type) ((type*)(ap+=sizeof(type)))[-1]
#define va_end(ap)

Try to figure out how to modify that without using any extensions to C
such as sizeof(promoted_typeof(type)). ...
The 'extension' is static type analysis, indeed, the *same* analysis
used to promote parameters in variadic function calls in the first
place.

Yes, but while this is available to the compiler (internally),
there is no existing C construct that requires that it be exposed
to the programmer. A __sizeof_promoted_type operator *ought* to
be trivial to add (because the compiler has to know how to do this
inside), but it does have to be added. (It is slightly worse than
this; see the end of this article.)

All that said, back in 1989, the X3J11 committee actually changed
the way one used what was then <varargs.h>, adding some required
explicit syntax -- the ", ..." part of a prototype -- and changing
the va_start() macro so that it takes two arguments instead of one.
Having done that, I always thought it was silly not to go the rest
of the way, and make "..." act like a special purpose variable:

#define va_start(ap) ((ap) = &...)

While va_start() could be retained (from <varargs.h> in the new
<stdarg.h>), the C committee could have written that this is the
one and only correct way to #define it. The "variable" named ...
would thus become a non-modifiable, indescribably-typed lvalue
whose only allowed operation is "take its address", producing a
value of whatever type "va_list" is.

Note that this -- removing the second argument to va_start() --
neatly expresses the constraint C imposes, that there only be a
single set of varying arguments. Having that second argument
implies (inappropriately) that one might be able to start the
varying portion at different points. Furthermore, with this "&..."
change, the requirement for at least one fixed argument could also
be dropped, giving us the syntax:

void g(...) {
va_list ap;

ap = &...; /* inline expansion of va_start(ap) */
/* code here */
va_end(ap);
}

This kind of function makes sense in some contexts, e.g., when
the body is something like:

char *p, *ret;
size_t len = 0;

while ((p = va_arg(ap, char *)) != NULL)
len += strlen(p);
va_end(ap);
ret = malloc(len + 1);
if (ret != NULL) {
char *tail = ret;

va_start(ap);
while ((p = va_arg(ap, char *)) != NULL) {
/* can do this in one line with sprintf, but is slow */
strcpy(tail, p);
tail += strlen(tail);
}
va_end(ap);
*tail = '\0'; /* in case there are no strings */
}

and of course a final "return ret".

The issue of whether to require promoted types to va_arg() could
remain separate. Requiring implementations to provide a "size of
promoted type" keyword, on implementations that use something as
simple as the va_arg() macro described above, is not much of a
hardship -- and for implementations that use "secret" compiler
tricks to access the varying arguments, doing the promotion is
still not a hardship. I for one think it would be an improvement:
not necessarily an *important* one, but in some ways, a "good"
language is one in which all the minor irritants have been removed
wherever feasible. One of J&W Pascal's original nasty failings
was the lack of a default case. Yes, you can work around it with
an "if" in front of the switch/case -- but this is a pesky thorn,
and there is no reason not to remove it from the lion's foot.

Assuming we *were* to add a special new keyword or two, here is
one way might rewrite va_arg:

/* original:
va_arg(ap, ty) (((ty *)(ap += sizeof(ty)))[-1])
new: */
#define va_arg(ap, ty) ((ty)((__convert_to_ptr_to_promoted(ty, \
ap += __sizeof_promoted(ty)))[-1]))

This also removes the pesky thorn that the "type" parameter to
va_arg be syntactically valid after suffixing with "*". :)
 
B

Brian Inglis

Peter said:
I wonder why va_arg could not accept a type subject to promotion and
handle them accordingly, i.e. accept the promoted type implicitly and
convert it to the original/requested type. [So, cases like the
character types and unsigned short, which might promote to signed or
unsigned int, could be handled without having to resort to integer
limit checks.]

While the programmer should know that the type char and short are
promoted, what about int32_t? Can it be an argument of va_arg? It
might be equivalent to a short (needing promotion), or a int or a long.
Is there any way of portably accessing a variadic int32_t (or intX_t for
X >= 16) parameter?

A long is guaranteed to hold at least the same values as an
int32_t, int and short may not. A short is guaranteed to hold at
least the same values as an int16_t. A signed char is guaranteed
to hold at least the same values as an int8_t. There are no other
guarantees AFAIR.

Thanks. Take care, Brian Inglis Calgary, Alberta, Canada
 
F

Fergus Henderson

A tedious one...

#if INT32_MAX <= INT_MAX
int32_t x = va_arg(ap, int);
#else
int32_t x = va_arg(ap, int32_t);
#endif

Well, if that works, you could do it a little less tediously:

#define promoted_va_arg(ap, type, max) \
((max) <= INT_MAX ? va_arg((ap), int) : va_arg((ap), (type)))

int32_t x = promoted_va_arg(ap, int32_t, INT32_MAX);
 
D

Dan Pop

In said:
Peter said:
I wonder why va_arg could not accept a type subject to promotion and
handle them accordingly, i.e. accept the promoted type implicitly and
convert it to the original/requested type. [So, cases like the
character types and unsigned short, which might promote to signed or
unsigned int, could be handled without having to resort to integer
limit checks.]

While the programmer should know that the type char and short are
promoted, what about int32_t? Can it be an argument of va_arg? It
might be equivalent to a short (needing promotion), or a int or a long.
Is there any way of portably accessing a variadic int32_t (or intX_t for
X >= 16) parameter?

It is precisely this kind of issues that render the typedef'ed "types" a
lot less useful and easy to use than their fans believe.

If I multiply two size_t values, do I get undefined behaviour in case of
overflow or the result modulo SIZE_MAX? It depends on whether size_t is
subject to the integral promotions or not, an issue the standard is
not addressing at all.

Dan
 
D

Dan Pop

In said:
I don't disagree with that. This program is part of a widely distributed
program library, and I am sure has been compiled on many compilers over the
years. Most likely, this statement has never been executed, so it really
doesn't matter what it compiles into. The problem comes when a program is
supposed to be portable to a wide variety of machines and compilers, with
the expectation that it can be compiled and installed by non-C programmers.

A non-C programmer has no business using a C compiler or building and
installing C programs. Imagine that the compiler emits a warning
(something that the person who wrote the code cannot avoid, for *any* C
compiler). Without knowing C, how can you tell if the problem can be
safely ignored, or the resulting executable should not be used?
Someone is supposed to be able to untar it, run make, and not have any
compiler or link errors.

Even required diagnostics can be produced as "warnings" instead of
"errors", so the absence of compiler errors means exactly nothing.
Considering that it passed so many compilers on
the way, would it have been reasonable to make it a warning?

When the code is obviously broken, an error is much better. Far too many
people blissfully ignore the warnings...
The next error I ran into, in a different library, came from not finding the
include file ndbm.h. It seems that it is now db.h, at least on the system
that I was using.

Another argument for having a C programmer building C programs. Similar
issues arise with -lcurses vs -lncurses at link time. Again obvious for
the C programmer and totally insurmountable for the non-C programmer.

Dan
 

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,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top