Pregunta ¿Son estos tipos de funciones compatibles en C?


Considere el siguiente programa C:

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

De acuerdo con el estándar C11, Sec. 6.5.16.1, en una asignación simple, "se mantendrá uno de los siguientes", y el único relevante en la lista es el siguiente:

el operando izquierdo tiene un tipo de puntero atómico, calificado o no calificado, y (considerando el tipo que tendría el operando izquierdo después de la conversión lvalue) ambos operandos son punteros a versiones calificadas o no calificadas de tipos compatibles, y el tipo al que apunta la izquierda tiene todos los calificadores del tipo señalado por la derecha;

Además, esta es una "restricción", es decir, una implementación conforme debe informar un mensaje de diagnóstico si se infringe.

Me parece que esta restricción se viola en la asignación en el programa anterior. Ambos lados de la tarea son indicadores de función. Entonces la pregunta es, ¿son compatibles los dos tipos de funciones? Esto se responde en Sec. 6.7.6.3:

Para que dos tipos de funciones sean compatibles, ambos deberán especificar tipos de retorno compatibles.146) Además, las listas de tipos de parámetros, si ambas están presentes, acordarán el número de parámetros y el uso del terminador de puntos suspensivos; los parámetros correspondientes deben tener tipos compatibles. Si un tipo tiene una lista de tipos de parámetros y el otro tipo está especificado por un declarador de funciones que no es parte de una definición de función y que contiene una lista de identificadores vacía, la lista de parámetros no tendrá un terminador de puntos suspensivos y el tipo de cada parámetro ser compatible con el tipo que resulta de la aplicación de las promociones de argumento predeterminadas. Si un tipo tiene una lista de tipos de parámetros y el otro tipo está especificado por una definición de función que contiene una lista de identificadores (posiblemente vacía), ambos acordarán en el número de parámetros, y el tipo de cada parámetro prototipo será compatible con el tipo que resulta de la aplicación de las promociones de argumento predeterminadas al tipo del identificador correspondiente.

En este caso, uno de los tipos, el de h1, tiene una lista de tipos de parámetros; el otro, f, no. Por lo tanto, se aplica la última oración de la cita anterior: en particular, "ambos acordarán en el número de parámetros". Claramente h1 toma un parámetro. ¿Qué hay de f? El siguiente punto ocurre justo antes de lo anterior:

Una lista vacía en un declarador de función que es parte de una definición de esa función especifica que la función no tiene parámetros.

Entonces, claramente f toma 0 parámetros. Entonces, los dos tipos no concuerdan en el número de parámetros, los dos tipos de funciones son incompatibles, y la asignación viola una restricción y se debe emitir un diagnóstico.

Sin embargo, tanto gcc 4.8 como Clang no emiten advertencias al compilar el programa:

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

Por cierto, ambos compiladores emiten advertencias si f se declara "int f (void) ...", pero esto no debería ser necesario según mi lectura del Estándar anterior.

Las preguntas:

P1: ¿La asignación "h1 = f"? en el programa anterior viola la restricción "ambos operandos son punteros a versiones calificadas o no calificadas de tipos compatibles"? Específicamente:

Q2: El tipo de h1 en la expresión "h1 = f" es puntero-a-T1 para algún tipo de función T1. ¿Qué es exactamente T1?

Q3: El tipo de f en la expresión "h1 = f" es puntero-a-T2 para algún tipo de función T2. ¿Qué es exactamente T2?

P4: ¿Son compatibles los tipos T1 y T2? (Indique las secciones apropiadas del Estándar u otros documentos para respaldar la respuesta).

Q1 ', Q2', Q3 ', Q4': Ahora supongamos que la declaración de f se cambia a "int f (void) {return 9;}". Responda las preguntas 1-4 nuevamente para este programa.


32
2017-07-14 19:04


origen


Respuestas:


Estos dos informes de defectos abordan su problema:

Informe de defectos 316 dice (énfasis mío en el futuro)

Las reglas para la compatibilidad de tipos de funciones en 6.7.5.3 # 15 no haga   definir cuando un tipo de función es "especificado por una definición de función   que contiene una lista de identificadores (posiblemente vacía) ", [...]

y tiene un ejemplo similar al que das:

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

y continúa diciendo:

Creo que la intención del estándar es que tipo es especificado por un   definición de función solo a los fines de verificar la compatibilidad de   declaraciones múltiples de la misma función; cuando como aquí el nombre de   la función aparece en una expresión, su tipo está determinado por su   return type y no contiene ningún rastro de los tipos de parámetros. Sin embargo,   las interpretaciones de implementación varían.

Pregunta 2: ¿Es válida la unidad de traducción anterior?

y la respuesta del comité fue:

El Comité cree que las respuestas a Q1 y 2 son sí

Esto fue entre C99 y C11, pero el comité agrega:

No tenemos intención de arreglar las viejas reglas de estilo. sin embargo, el   las observaciones hechas en este documento parecen ser generalmente correctas.

y hasta donde puedo decir que C99 y C11 no difieren mucho en las secciones que ha citado en la pregunta. Si miramos más a fondo informe de defectos 317 podemos ver que dice:

Creo que la intención de C es que las definiciones de funciones de estilo antiguo con   paréntesis vacíos no le dé a la función un tipo que incluya un   prototipo para el resto de la unidad de traducción. Por ejemplo:

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

Pregunta 1: ¿Dicha definición de función da a la función un tipo   incluyendo un prototipo para el resto de la unidad de traducción?

Pregunta 2: ¿Es válida la unidad de traducción anterior?

y la respuesta de los comités fue:

La respuesta a la pregunta n. ° 1 es NO, y la pregunta n. ° 2 es SÍ. Existen   sin infracciones de restricciones, sin embargo, si la llamada a la función se ejecutó   tendría un comportamiento indefinido. Ver 6.5.2.2; p6.

Esto parece depender del hecho de que no está especificado si una definición de función define un tipo o un prototipo y, por lo tanto, significa que no hay requisitos de verificación de compatibilidad. Este fue originalmente el intento con las definiciones de funciones de estilo antiguo y el comité no aclarará más probablemente porque está en desuso.

El comité señala que el hecho de que la unidad de traducción sea válida no significa que no haya un comportamiento indefinido.


8
2017-07-16 03:22



Históricamente, los compiladores de C generalmente manejaban el paso de argumentos de una manera que garantizaba que los argumentos adicionales serían ignorados, y también solo requería que los programas pasaran argumentos para parámetros que en realidad eran usado, permitiendo así, por ejemplo,

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

para ser rescatado de forma segura a través de cualquiera foo(1,123); o foo(0);, sin tener que especificar un segundo argumento en este último caso. Incluso en las plataformas (por ejemplo, Macintosh clásico) cuya convención de llamadas normal no admitiría tal garantía, los compiladores de C suelen utilizar una convención de llamadas que lo respalde.

El estándar deja en claro que los compiladores no son necesario para soportar tal uso, pero requerir implementaciones para prohibirlos, no solo habría roto el código existente, sino que también habría imposibilitado que esas implementaciones produjeran código que haya sido tan eficiente como lo que era posible en C pre-estándar (ya que el código de la aplicación tendría que ser cambiado para pasar argumentos inútiles, que los compiladores tendrían que generar código para). Hacer tal uso Undefined Behavior alivió las implementaciones de cualquier obligación de soporte, al tiempo que permite que las implementaciones lo admitan si es conveniente.


2
2017-10-26 23:26



No es una respuesta directa a su pregunta, pero el compilador simplemente genera un ensamblado para insertar el valor en la pila antes de llamar a la función.

Por ejemplo (usando el compilador VS-2013):

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

Si agrega una variable local en esta función, puede usar su dirección para encontrar los valores que pase cada vez que llame a la función.

Por ejemplo (usando el compilador VS-2013):

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);
}

Entonces, en esencia, llamar a la función con argumentos adicionales es completamente seguro, ya que simplemente se insertan en la pila antes de que el contador de programas se establezca en la dirección de la función (en la sección de códigos de la imagen ejecutable).

Por supuesto, se podría afirmar que podría dar como resultado un desbordamiento de la pila, pero eso puede suceder en cualquier caso (incluso si la cantidad de argumentos pasados ​​es la misma que la cantidad de argumentos declarados).


1
2017-07-15 07:26



Para las funciones sin parámetros declarados, el compilador no deduce parámetros / tipos de parámetros. El siguiente código es esencialmente el mismo:

int f()
{
    return 9;
}

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

Creo que esto tiene algo que ver con la forma subyacente en que se soportan los argumentos de longitud variable, y eso () es básicamente idéntico a (...). Al observar de cerca el código de objeto generado, se muestra que los argumentos de f () aún se insertan en los registros utilizados para llamar a la función, pero como se referencian en la definición de función, simplemente no se usan dentro de la función. Si desea declarar un parámetro que no admite argumentos, es un poco más apropiado escribirlo como tal:

int f(void)
{
    return 9;
}

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

Este código no se compilará en GCC por el siguiente error:

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

-1
2017-07-14 22:32



intente usar __stdcall antes de la declaración de la función, y no compilaría.
El motivo es que la llamada a la función es __cdecl por defecto. Significa (además de otras características) que la persona que llama borra la pila después de la llamada. Por lo tanto, la función de llamada puede presionar en la pila todo lo que quiera, ya que sabe lo que empujó y eliminará la pila de la manera correcta.
__stdcall significa (además de otras cosas) que el destinatario limpiaría la pila. Entonces el número de argumentos debe coincidir.
... el letrero le dice al compilador que la cantidad de argumentos varía. Si se declara como __stdcall, entonces se sustituirá automáticamente por __cdecl, y aún puede usar tantos argumentos como desee.

Es por eso que el compilador advierte, pero no se detiene.

Ejemplos
Error: la pila está corrupta.

#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);
}

Trabajos

#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);
}

Para este ejemplo, tiene un comportamiento normal, que no está interconectado con el estándar. Usted declara el puntero a la función, después de eso asigna este puntero y conduce a la conversión de tipo implícito. Escribí por qué funciona. En c también puedes escribir

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

Y todavía es parte del estándar, pero otra parte del estándar, por supuesto. Y no pasa nada, incluso si asigna el puntero para funcionar al puntero a int.


-4
2017-07-15 05:40