Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are these compatible function types in C?

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.

like image 557
Steve Siegel Avatar asked Jul 14 '14 19:07

Steve Siegel


People also ask

What are compatible types in C?

In C, compatible types are defined as: two types that can be used together without modification (as in an assignment expression) two types that can be substituted one for the other without modification.

Are N and C compatible?

The 'normal', 2.5 amp type C plug fits perfectly into a type E, F, H, J, K, L, N or O socket.

What do you mean by type compatibility?

Compatibility between types refers to the similarity of two types to each other. Type compatibility is important during type conversions and operations. All valid declarations in the same scope that refer to the same object or function must have compatible types.

Do functions have types?

Every function has a specific function type, made up of the parameter types and the return type of the function. For example: func addTwoInts(_ a: Int, _ b: Int) -> Int { return a + b.


3 Answers

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.

like image 130
Shafik Yaghmour Avatar answered Oct 17 '22 11:10

Shafik Yaghmour


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.

like image 38
supercat Avatar answered Oct 17 '22 10:10

supercat


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).

like image 21
barak manos Avatar answered Oct 17 '22 09:10

barak manos