views:

575

answers:

4

This question is about vararg functions, and the last named parameter of them, before the ellipsis:

void f(Type paramN, ...) {
  va_list ap;
  va_start(ap, paramN);
  va_end(ap);
}

I was reading in the C Standard, and found the following restriction for the va_start macro:

The parameter parmN is the identifier of the rightmost parameter in the variable parameter list in the function definition (the one just before the , ...). If the parameter parmN is declared with the register storage class, with a function or array type, or with a type that is not compatible with the type that results after application of the default argument promotions, the behavior is undefined.

I wonder why the behavior is undefined for the following code

void f(int paramN[], ...) {
  va_list ap;
  va_start(ap, paramN);
  va_end(ap);
}

and not undefined for the following

void f(int *paramN, ...) {
  va_list ap;
  va_start(ap, paramN);
  va_end(ap);
}

The macros are intended to be implementable by pure C code. But pure C code cannot find out whether or not paramN was declared as an array or as a pointer. In both cases, the type of the parameter is adjusted to be a pointer. The same is true for function type parameters.

I wonder: What is the rationale of this restriction? Do some compilers have problems with implementing this when these parameter adjustments are in place internally? (The same undefined behavior is stated for C++ - so my question is about C++ aswell).

+2  A: 

It's not undefined. Keep in mind that when parameter is declared as int paramN[], the actual parameter type will still decay to int* paramN immediately (which is visible in C++, for example, if you apply typeid to paramN).

I must admit that I'm not sure what this bit in the spec is even for, considering that you cannot have parameters of function or array types in the first place (since they will pointer-decay).

Pavel Minaev
The text says "If the parameter parmN is declared with [...] a function or array type [...] the behavior is undefined.". I think that applies to my second last snippet containing the `int paramN[]`, if i'm not mistaken. Also "man va_start" says: "Because the address of this argument may be used in the va_start() macro, it should not be declared as a register variable, or as a function or an array type.". Weird
Johannes Schaub - litb
+1; this is exactly my analysis. It's not clear to me what the text could be referring to (but then, this is precisely the OP's question).
Martin v. Löwis
+1  A: 

I can only guess that the register restriction is there to ease library/compiler implementation -- it eliminates a special case for them to worry about.

But I have no clue about the array/function restriction. If it were in the C++ standard only, I would hazard a guess that there is some obscure template matching scenario where the difference between a parameter of type T[] and of type T* makes a difference, correct handling of which would complicate va_start etc. But since this clause appears in the C standard too, obviously that explanation is ruled out.

My conclusion: an oversight in the standards. Possible scenario: some pre-standard C compiler implemented parameters of type T[] and T* differently, and the spokesperson for that compiler on the C standards committee had the above restrictions added to the standard; that compiler later became obsolete, but no-one felt the restrictions were compelling enough to update the standard.

j_random_hacker
Johannes Schaub - litb
"In practice, many current implementations have “hidden machinery” that is used by the va_start macro to diagnose incorrect usage (for example, to verify that parmN actually is the name of the last fixed parameter) or to handle more complex argument passing mechanisms. Such machinery would be capable of handling any kind of parameter without restriction, but the C89 Committee saw no compelling reason to lift these restrictions, as that would require all implementations to have such machinery." This also seems to indicate it's for compatibility. I wonder what the problem for the compilers is
Johannes Schaub - litb
An additional thing hinting at a compatibility with pre-C89 compilers, <stdarg.h> is one of the committee inventions. Previously there were several mechanisms, <varargs.h> being most widely known (mentioned in PJ Plauger's "The standard C library", he doesn't comment the restriction).
AProgrammer
I don't understand why people consider this response helpful - I assume the OP might have guessed as much.
Martin v. Löwis
+5  A: 

The restriction against register parameters or function parameters are probably something like:

  • you are not allowed to take the address of a variable with the register storage class.
  • function pointers are sometimes quite different than pointers to objects. For example, they might be larger than pointers to objects (you can't reliably convert a function pointer to an object pointer and back again), so adding some fixed number to the address of a function pointer might not get you to the next parameter. If va_start() and/or va_arg() were implemented by adding some fixed amount to the address of paramN and function pointers were larger than object pointers the calculation would end up with the wrong address for the object va_arg() returns. This might not seem to be a great way to implement these macros, but there might be platforms that have (or even need) this type of implementation.

I can't think of what the problem would be to prevent allowing array parameters, but PJ Plauger says this in his book "The Standard C Library":

Some of the restrictions imposed on the macros defined in <stdarg.h> seem unnecessarily severe. For some implementations, they are. Each was introduced, however, to meet the needs of at least one serious C implementation.

And I imagine that there are few people who know more about the ins and outs of the C library than Plauger. I hope someone can answer this specific question with an actual example; I think it would be an interesting bit of trivia.

New info:


The "Rationale for International Standard - Programming Languages - C" says this about va_start():

The parmN argument to va_start was intended to be an aid to implementors writing the definition of a conforming va_start macro entirely in C, even using pre-C89 compilers (for example, by taking the address of the parameter). The restrictions on the declaration of the parmN parameter follow from the intent to allow this kind of implementation, as applying the & operator to a parameter name might not produce the intended result if the parameter’s declaration did not meet these restrictions.

Not that that helps me with the restriction on array parameters.

Michael Burr
What's weird is that it allows `void f(void(*p)(), ...);` which is a function pointer too. If the problem is that it is a function pointer, why don't they forbid function pointers in general, declared using pointer syntax as-well? +1 for the Plauger quote, though :)
Johannes Schaub - litb
Function pointers _should_ follow the same "`+1` is converted to `+sizeof(type)`" rule, because function pointers _are_ pointers and you _can_ make arrays of them. Or at least, I can. I have no standards quote for this.
Chris Lutz
i think he means that the sizeof of a function pointer is potentially different than that of a object pointer. so if some offset somewhere is calculated by simply adding `sizeof(void*)` if the parameter has pointer type, things could break. But the same problem would seem to exist for a parameter declared with pointer to function type. So restricting it to function types seems weird to me.
Johannes Schaub - litb
Michael Burr
Michael Burr
@Michael Burr, that just says that when you have an expression having function type, it decays when not used in sizeof (where it is illegal) or unary ` ? The undefined behavior for the second argument to `va_start` happens only if `paramN` was declared with a function type. However, `p` was declared with a pointer to function type.
Johannes Schaub - litb
Johannes Schaub - litb
But this is said about function parameters in function declarations: "A declaration of a parameter as 'function returning type' shall be adjusted to 'pointer to function returning type', as in 6.3.2.1", which I read as essentially saying there's no such thing as a parameter with function type - they're really pointers to function.
Michael Burr
That's true. A parameter type never has function type or array type (because of that adjustments). But the parameter is still be declared as a function and as an array respectively (and C99 adds useful things like "void f(int n[static 100]);" where you may actually benefit of declaring it as such). The type of things and how you declare them are not always the same. For example declaring an array as "int a[];" at file-scope will give it type "int[1];" when there is no subsequent (tentative) definition giving a size until the end of the TU.
Johannes Schaub - litb
Maybe they just omit "pointer to function type" in the UB-case, because it's the same anyway as "function type" in the parameter list? But that then would mean that any pointer used is undefined behavior, because `T[] == T*` then and `T[]` is an array type. That would be very weird though.
Johannes Schaub - litb
+3  A: 

I found another relevant quote, from Dinkumware.

The last parameter must not have register storage class, and it must have a type that is not changed by the translator. It cannot have:

* an array type
* a function type
* type float
* any integer type that changes when promoted
* a reference type [C++ only]

So apparently, the problem is precisely that the parameter gets passed in a way different from how it is declared. Interestingly enough, they also ban float and short, even though those should be supported by the standard.

As a hypothesis, it could be that some compilers have problems doing sizeof correctly on such parameters. E.g. it might be that, for

int f(int x[10])
{
        return sizeof(x);
}

some (buggy) compiler will return 10*sizeof(int), thus breaking the va_start implementation.

Martin v. Löwis
That they ban `float` is clear, because `float` is changed by default argument promotion to `double`. Likewise is `short` promoted. What i find particular interesting: You may have a prototype `void f(void g());` in one TU, and a definition like `void f(void(*g)()) { .. }` in another TU, and calling the function with that definition is fine too. Apparently, there must be a notion of a "function designator value" other than a function pointer such that the matter renders the situation for varargs invalid. The incorrect-sizeof-calculation looks like a possible thingy too. So +1 xD
Johannes Schaub - litb
As far as I can tell, that Plauger quote is just a (near-)restatement of the statement in the C standard, not an explanation of why things are that way. Your speculation seems reasonable to me, though it is just speculation (be careful, it seems some will downvote for that reason :P)
j_random_hacker
@litb: My guess is that the standard designers wanted to allow the library implementor to write `sizeof arg2` in the `va_start` macro even if the compiler was buggy (in which case the compiler might have trouble with the tokens `sizeof arg2` if arg2 is declared as `void arg2()`). The bug I'm hypothesising here is similar to Martin's proposed bug -- a case of the compiler incorrectly using the "as-declared" type of an object rather than the type it would have in an expression.
j_random_hacker
AProgrammer
@j_random_hacker: I find the Dinkumware library documentation interesting because it a) does give a rationale (the declared type must not be changed), and b) it also includes types that standard C doesn't ban.
Martin v. Löwis
The C standard does ban types modified by promotion (and float is one). See litb's quote in his question.
AProgrammer
@AProgrammer: ah, I had missed that so far.
Martin v. Löwis
@Martin, although the Dinkumware manual sounds more like a description of the situation, than like an actual explanation of the reason. The only thing it contains in addition to the Standard is "it must have a type that is not changed by the translator". I would have liked if the dinkumware actually provides insight into that.
Johannes Schaub - litb