variadic without va_arg

B

Bill Pursell

I don't particularly enjoy using the va_start macro family, and I've
noticed that the following code works. I'm a little concerned about
the fact that the prototypes for foo do not match. Is this safe?

[tmp]$ cat q.c
extern int foo(int x,...);

int
main (int argc, char const*const* argv) /*:)*/
{
foo(0);
foo(1,8);
foo(2,3,7);
foo(4,1,2,3,4);
return 0;
}
[tmp]$ cat r.c

int
foo(int num, int a, int b, int c)
{
switch(num) {
case 0: return 0;
case 1: return a;
case 2: return a+b;
case 3: return a+b+c;
default: return -1;
}
}
 
D

Diomidis Spinellis

Bill said:
I don't particularly enjoy using the va_start macro family, and I've
noticed that the following code works. I'm a little concerned about
the fact that the prototypes for foo do not match. Is this safe?

[tmp]$ cat q.c
extern int foo(int x,...);

int
main (int argc, char const*const* argv) /*:)*/
{
foo(0);
foo(1,8);
foo(2,3,7);
foo(4,1,2,3,4);
return 0;
}
[tmp]$ cat r.c

int
foo(int num, int a, int b, int c)
{
switch(num) {
case 0: return 0;
case 1: return a;
case 2: return a+b;
case 3: return a+b+c;
default: return -1;
}
}

According to the C99 standard (section 6.9.1) "If a function that
accepts a variable number of arguments is defined without a parameter
type list that ends with the ellipsis notation, the behavior is undefined."

Your code works, because your compiler and processor architecture use an
argument passing convention compatible with the 1970s style C. At that
time functions like printf were indeed called with fewer or more
arguments than their specification and extra arguments or clever pointer
arithmetic were used to access the remaining arguments. These tricks
were however not portable, and prompted the development of the Unix
vararg and later the ANSI stdarg facilities.

Having said that I admit that processor architects and compiler vendors
go to extreme lengths to make legacy code work. I tried your example on
some architectures I thought it would bomb (a SPARC, an Itanium, and an
Alpha), and it worked correctly. Still, there's no reason to write
non-portable code: at the very least you demonstrate you're not playing
by the rules. If I was reading your code I would worry that other
problems might also be lurking in it.
 
M

Michael Mair

Bill said:
I don't particularly enjoy using the va_start macro family, and I've
noticed that the following code works. I'm a little concerned about
the fact that the prototypes for foo do not match. Is this safe?

[tmp]$ cat q.c
extern int foo(int x,...);

int
main (int argc, char const*const* argv) /*:)*/
{
foo(0);
foo(1,8);
foo(2,3,7);
foo(4,1,2,3,4);
return 0;
}
[tmp]$ cat r.c

int
foo(int num, int a, int b, int c)
{
switch(num) {
case 0: return 0;
case 1: return a;
case 2: return a+b;
case 3: return a+b+c;
default: return -1;
}
}

No, this comes into conflict with C99, 6.7.5.3#9 and #15;
you are using incompatible function types.
You are invoking UB.

Let us go for more practical reasons:
- If you do not use int, then you might run into unpleasant
surprises.
int foo(int num, long a, short b, char c)
with LONG_MAX > INT_MAX and sizeof long > sizeof int
int foo(int num, long double a, double b, float c)
both might give you trouble.
- In addition, passing hundred arguments to foo() may be
harmful in quite unexpected ways.
- Another thing: You might have different calling conventions,
maybe only for a certain number of parameters. Merriment ensues
for fixed parameter order.
- Your lint tool or linker warns you about it.

If you really dislike variable argument list handling that much,
then do not use variable argument lists -- you nearly always can
roll an alternative avoiding them at some cost.


Cheers
Michael
 
B

Bill Pursell

Diomidis said:
Bill said:
I don't particularly enjoy using the va_start macro family, and I've
noticed that the following code works. I'm a little concerned about
the fact that the prototypes for foo do not match. Is this safe?

[tmp]$ cat q.c
extern int foo(int x,...);

int
main (int argc, char const*const* argv) /*:)*/
{
foo(0);
foo(1,8);
foo(2,3,7);
foo(4,1,2,3,4);
return 0;
}
[tmp]$ cat r.c

int
foo(int num, int a, int b, int c)
{
switch(num) {
case 0: return 0;
case 1: return a;
case 2: return a+b;
case 3: return a+b+c;
default: return -1;
}
}

According to the C99 standard (section 6.9.1) "If a function that
accepts a variable number of arguments is defined without a parameter
type list that ends with the ellipsis notation, the behavior is undefined."

Your code works, because your compiler and processor architecture use an
argument passing convention compatible with the 1970s style C. At that
time functions like printf were indeed called with fewer or more
arguments than their specification and extra arguments or clever pointer
arithmetic were used to access the remaining arguments. These tricks
were however not portable, and prompted the development of the Unix
vararg and later the ANSI stdarg facilities.

Having said that I admit that processor architects and compiler vendors
go to extreme lengths to make legacy code work. I tried your example on
some architectures I thought it would bomb (a SPARC, an Itanium, and an
Alpha), and it worked correctly. Still, there's no reason to write
non-portable code: at the very least you demonstrate you're not playing
by the rules. If I was reading your code I would worry that other
problems might also be lurking in it.

Would it be portable to simply change the prototypes so that the caller
has the interface:
extern int foo(int num, ...)
while the definition of the function gets:
int foo(int num, int a, int b, int c, ...)?

That seems to satisfy the section of the standard you quote above, but
it still feels wrong.
 
D

Diomidis Spinellis

Bill said:
[...]

Would it be portable to simply change the prototypes so that the caller
has the interface:
extern int foo(int num, ...)
while the definition of the function gets:
int foo(int num, int a, int b, int c, ...)?

That seems to satisfy the section of the standard you quote above, but
it still feels wrong.

Still wrong; see Michael Mair's reply to your original post.
 
J

Jack Klein

I don't particularly enjoy using the va_start macro family, and I've
noticed that the following code works. I'm a little concerned about
the fact that the prototypes for foo do not match. Is this safe?

[tmp]$ cat q.c
extern int foo(int x,...);

int
main (int argc, char const*const* argv) /*:)*/
{
foo(0);
foo(1,8);
foo(2,3,7);
foo(4,1,2,3,4);
return 0;
}
[tmp]$ cat r.c

int
foo(int num, int a, int b, int c)
{
switch(num) {
case 0: return 0;
case 1: return a;
case 2: return a+b;
case 3: return a+b+c;
default: return -1;
}
}

This only "works" as you want because your compiler uses a method
required for pre-standard C for passing arguments. That may only be
true with the particular set of compiler options that you use. If you
change options, such as for optimization, it might well break.

There are implementations where this will not work under any
circumstances. They use completely different methods of passing
arguments to variadic and non-variadic functions. I have used several
such compilers over the years.
 

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

Forum statistics

Threads
473,769
Messages
2,569,581
Members
45,057
Latest member
KetoBeezACVGummies

Latest Threads

Top