Are these compatible function types in C?

安稳与你 提交于 2019-12-03 04:45:59

问题


Consider the following C program:

int f() { return 9; }
int main() {
  int (*h1)(int);
  h1 = f; // why is this allowed?                                               
  return h1(7);
}

According to the C11 Standard, Sec. 6.5.16.1, in a simple assignment, "one of the following shall hold", and the only relevant one in the list is the following:

the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) both operands are pointers to qualified or unqualified versions of compatible types, and the type pointed to by the left has all the qualifiers of the type pointed to by the right;

Moreover, this is a "constraint", meaning, a conforming implementation must report a diagnostic message if it is violated.

It seems to me that this constraint is violated in the assignment in the program above. Both sides of the assignment are function pointers. So the question is, are the two function types compatible? This is answered in Sec. 6.7.6.3:

For two function types to be compatible, both shall specify compatible return types.146) Moreover, the parameter type lists, if both are present, shall agree in the number of parameters and in use of the ellipsis terminator; corresponding parameters shall have compatible types. If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions. If one type has a parameter type list and the other type is specified by a function definition that contains a (possibly empty) identifier list, both shall agree in the number of parameters, and the type of each prototype parameter shall be compatible with the type that results from the application of the default argument promotions to the type of the corresponding identifier.

In this case, one of the types, that of h1, has a parameter type list; the other, f, does not. Hence the last sentence in the quote above applies: in particular, "both shall agree in the number of parameters". Clearly h1 takes one parameter. What about f? The following point occurs just before the above:

An empty list in a function declarator that is part of a definition of that function specifies that the function has no parameters.

So clearly f takes 0 parameters. So the two types do not agree in the number of parameters, the two function types are incompatible, and the assignment violates a constraint, and a diagnostic should be issued.

However, both gcc 4.8 and Clang emit no warnings when compiling the program:

tmp$ gcc-mp-4.8 -std=c11 -Wall tmp4.c 
tmp$ cc -std=c11 -Wall tmp4.c 
tmp$

By the way, both compilers do issue warnings if f is declared "int f(void) ...", but this should not be necessary based on my reading of the Standard above.

The questions:

Q1: Does the assignment "h1=f;" in the program above violate the constraint "both operands are pointers to qualified or unqualified versions of compatible types"? Specifically:

Q2: The type of h1 in the expression "h1=f" is pointer-to-T1 for some function type T1. What exactly is T1?

Q3: The type of f in the expression "h1=f" is pointer-to-T2 for some function type T2. What exactly is T2?

Q4: Are T1 and T2 compatible types? (Please quote appropriate sections of the Standard or other documents to support the answer.)

Q1', Q2', Q3', Q4': Now suppose the declaration of f is changed to "int f(void) { return 9; }". Answer questions 1-4 again for this program.


回答1:


These two defect reports address your issue:

  • Defect Report #316
  • Defect Report #317

Defect report 316 says (emphasis mine going forward):

The rules for compatibility of function types in 6.7.5.3#15 do not define when a function type is "specified by a function definition that contains a (possibly empty) identifier list", [...]

and it has a similar example to the one you give:

void f(a)int a;{}
void (*h)(int, int, int) = f;

and it goes on to say:

I believe the intent of the standard is that a type is specified by a function definition only for the purposes of checking compatibility of multiple declarations of the same function; when as here the name of the function appears in an expression, its type is determined by its return type and contains no trace of the parameter types. However, implementation interpretations vary.

Question 2: Is the above translation unit valid?

and the answer from the committee was:

The Committee believe the answers to Q1 & 2 are yes

This was between C99 and C11 but the committee adds:

We have no intention of fixing the old style rules. However, the observations made in this document seem to be generally correct.

and as far a I can tell C99 and C11 do not differ greatly in the sections you have quoted in the question. If we further look into defect report 317 we can see that it says:

I believe the intent of C is that old-style function definitions with empty parentheses do not give the function a type including a prototype for the rest of the translation unit. For example:

void f(){} 
void g(){if(0)f(1);}

Question 1: Does such a function definition give the function a type including a prototype for the rest of the translation unit?

Question 2: Is the above translation unit valid?

and the committees response was:

The answer to question #1 is NO, and to question #2 is YES. There are no constraint violations, however, if the function call were executed it would have undefined behavior. See 6.5.2.2;p6.

This seems to hinge on the fact that it is underspecified whether a function definition defines a type or a prototype and therefore means there is no compatibility checking requirements. This was originally the intent with old style function definitions and the committee will not clarify further probably because it is deprecated.

The committee points out that just because the translation unit is valid does not mean there is no undefined behavior.




回答2:


Historically, C compilers generally handled argument passing in a way that guaranteed that extra arguments would be ignored, and also only required that programs passed arguments for parameters that were actually used, thus allowing e.g.

int foo(a,b) int a,b;
{
  if (a)
    printf("%d",b);
  else
    printf("Unspecified");
}

to be safely callable via either foo(1,123); or foo(0);, without having to specify a second argument in the latter case. Even on platforms (e.g. classic Macintosh) whose normal calling convention wouldn't support such a guarantee, C compilers generally default to using a calling convention that would support it.

The Standard makes clear that compilers are not required to support such usage, but requiring implementations to forbid them would have not only broken existing code, but would also have made it impossible for those implementations to produce code that had been as efficient as what was possible in pre-standard C (since application code would have to be changed to pass useless arguments, which compilers would then have to generate code for). Making such usage Undefined Behavior relieved implementations of any obligation to support it, while still allowing implementations to support it if convenient.




回答3:


Not a direct answer to your question, but the compiler simply generates assembly for pushing the value into the stack before calling the function.

For example (using VS-2013 compiler):

mov         esi,esp
push        7
call        dword ptr [h1]

If you add a local variable in this function, then you can use its address in order to find the values that you pass whenever you call the function.

For example (using VS-2013 compiler):

int f()
{
    int a = 0;
    int* p1 = &a + 4; // *p1 == 1
    int* p2 = &a + 5; // *p2 == 2
    int* p3 = &a + 6; // *p3 == 3
    return a;
}

int main()
{
    int(*h1)(int);
    h1 = f;
    return h1(1,2,3);
}

So in essence, calling the function with additional arguments is completely safe, as they are simply pushed into the stack before the program-counter is set to the address of the function (in the code-section of the executable image).

Of course, one could claim that it might result with a stack-overflow, but that can happen in any case (even if the number of arguments passed is the same as the number of arguments declared).




回答4:


For functions without declared parameters no parameters/parameter types are inferred by the compiler. The following code is essentially the same:

int f()
{
    return 9;
}

int main()
{
    return f(7, 8, 9);
}

I believe this has something to do with the underlying way variable length arguments are supported, and that () is basically identical to (...). Taking a closer look at the generated object code shows that the arguments to f() still get pushed onto the registers used to call the function, but since they are referenced in the function definition they simply aren't used inside the function. If you want to declare a parameter that does not support arguments it's a bit more proper to write it as such:

int f(void)
{
    return 9;
}

int main()
{
    return f(7, 8, 9);
}

This code will fail to compile in GCC for the following error:

In function 'main':
error: too many arguments to function 'f'



回答5:


try to use __stdcall before function declaration - and it wouldn't compile.
The reason is that function call is __cdecl by default. It means (beside other features) that caller clears stack after call. So, caller function may push on stack everything it wants, because it knows what it had pushed and will clear stack in right way.
__stdcall means (beside other things) that callee would clean stack. So number of arguments must match.
... sign says to compiler that number of arguments varies. If declared as __stdcall, then it would be automatically substituted with __cdecl, and you still can use as many arguments as you want.

That is why compiler warns, but not halt.

Examples
Error: stack corrupted.

#include <stdio.h>

void __stdcall allmyvars(int num) {
    int *p = &num + 1;
    while (num--) {
        printf("%d ", *p);
        p++;
    }  
}

void main() {
    allmyvars(4, 1, 2, 3, 4);
}

Works

#include <stdio.h>

void allmyvars(int num) {
    int *p = &num + 1;
    while (num--) {
        printf("%d ", *p);
        p++;
    }  
}

void main() {
    allmyvars(4, 1, 2, 3, 4);
}

For this example you have normal behaviour, which is not interconnected with standard. You declare pointer to function, after that assign this pointer and it leads to implicit type conversion. I wrote why it works. In c you also can write

int main() {
  int *p;
  p = (int (*)(void))f; // why is this allowed?      
  ((int (*)())p)();
  return ((int (*)())p)(7);
}

And it is still part of standard, but other part of standard of course. And nothing happens, even if you assign pointer to function to pointer to int.



来源:https://stackoverflow.com/questions/24743887/are-these-compatible-function-types-in-c

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!