32 votes

Ces types de fonctions sont-ils compatibles avec le langage C ?

Considérons le programme C suivant :

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

Selon la norme C11, Sec. 6.5.16.1, dans une affectation simple, "l'un des éléments suivants doit tenir", et le seul élément pertinent de la liste est le suivant :

l'opérande gauche a un type de pointeur atomique, qualifié ou non qualifié, et (en considérant le type que l'opérande gauche aurait après la conversion en lvalue) les deux opérandes sont des pointeurs vers des versions qualifiées ou non qualifiées de types compatibles, et le type pointé par le gauche a tous les qualificatifs du type pointé par le droit ;

De plus, il s'agit d'une "contrainte", ce qui signifie qu'une mise en œuvre conforme doit signaler un message de diagnostic si elle est violée.

Il me semble que cette contrainte est violée dans l'affectation du programme ci-dessus. Les deux côtés de l'affectation sont des pointeurs de fonction. La question est donc de savoir si les deux types de fonctions sont compatibles. La réponse à cette question se trouve à la Sec. 6.7.6.3 :

Pour que deux types de fonction soient compatibles, les deux doivent spécifier des types de retour compatibles.146) En outre, les listes de types de paramètres, si elles sont toutes deux présentes, doivent concorder quant au nombre de paramètres et à l'utilisation du terminateur d'ellipse ; les paramètres correspondants doivent avoir des types compatibles. Si un type possède une liste de types de paramètres et que l'autre type est spécifié par un déclarateur de fonction qui ne fait pas partie d'une définition de fonction et qui contient une liste d'identifiants vide, la liste de paramètres ne doit pas comporter de terminaison en ellipse et le type de chaque paramètre doit être compatible avec le type qui résulte de l'application des promotions d'arguments par défaut. Si un type possède une liste de paramètres et que l'autre type est spécifié par une définition de fonction qui contient une liste d'identificateurs (éventuellement vide), les deux types doivent s'accorder sur le nombre de paramètres, et le type de chaque prototype de paramètre doit être compatible avec le type qui résulte de l'application des promotions d'arguments par défaut au type de l'identificateur correspondant.

Dans ce cas, l'un des types, celui de h1, a une liste de types de paramètres ; l'autre, f, n'en a pas. Par conséquent, la dernière phrase de la citation ci-dessus s'applique : en particulier, "les deux doivent s'accorder sur le nombre de paramètres". Il est clair que h1 prend un paramètre. Qu'en est-il de f ? Le point suivant intervient juste avant la citation ci-dessus :

Une liste vide dans un déclarateur de fonction qui fait partie d'une définition de cette fonction spécifie que la fonction n'a pas de paramètres.

Donc clairement f prend 0 paramètres. Donc les deux types ne s'accordent pas sur le nombre de paramètres, les deux types de fonction sont incompatibles, et l'affectation viole une contrainte, et un diagnostic doit être émis.

Cependant, gcc 4.8 et Clang n'émettent aucun avertissement lors de la compilation du programme :

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

À propos, les deux compilateurs émettent des avertissements si f est déclaré "int f(void) ...", mais cela ne devrait pas être nécessaire d'après ma lecture de la norme ci-dessus.

Les questions :

Q1 : L'affectation "h1=f ;" dans le programme ci-dessus viole-t-elle la contrainte "les deux opérandes sont des pointeurs vers des versions qualifiées ou non qualifiées de types compatibles" ? Plus précisément :

Q2 : Le type de h1 dans l'expression "h1=f" est pointeur-à-T1 pour un certain type de fonction T1. Quel est exactement le type T1 ?

Q3 : Le type de f dans l'expression "h1=f" est pointeur-à-T2 pour un certain type de fonction T2. Quel est exactement le type T2 ?

Q4 : Les types T1 et T2 sont-ils compatibles ? (Veuillez citer les sections appropriées de la norme ou d'autres documents pour étayer la réponse).

Q1', Q2', Q3', Q4' : Supposons maintenant que la déclaration de f soit modifiée en "int f(void) { return 9 ; }". Répondez à nouveau aux questions 1 à 4 pour ce programme.

1 votes

Si je mets cela dans clang j'obtiens : functCheck.cxx:4:6 : error : assigning to 'int (*)(int)' from incompatible type 'int ()' : different number of parameters (1 vs 0) h1 = f ; // why is this allowed... ^ ~ 1 erreur générée.

6 votes

@user2950041 : C'est une question sur le C. Le C a des notions de déclaration, de prototype et de définition différentes de celles du C++.

0 votes

@KerrekSB Bien sûr. C'est pourquoi j'ai utilisé un compilateur C, j'ai juste la mauvaise habitude de terminer les fichiers par .cxx. Le compilateur s'en moque

8voto

Shafik Yaghmour Points 42198

Ces deux rapports de défaut traitent de votre problème :

Rapport de défaut 316 dit ( l'accent est mis sur l'avenir ):

Les règles de compatibilité des types de fonctions dans 6.7.5.3#15 ne définir quand un type de fonction est "spécifié par une définition de fonction". qui contient une liste d'identifiants (éventuellement vide)", [...]

et il contient un exemple similaire à celui que vous donnez :

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

et il continue en disant :

Je crois que l'intention de la norme est qu'un est spécifié par une définition de fonction définition de fonction uniquement dans le but de vérifier la compatibilité de déclarations multiples de la même fonction ; lorsque, comme ici, le nom de la fonction de la fonction apparaît dans une expression, son type est déterminé par son type de retour et ne contient aucune trace des types des paramètres. Cependant, les interprétations des implémentations varient.

Question 2 : L'unité de traduction ci-dessus est-elle valable ?

et la réponse du comité a été :

Le Comité estime que les réponses aux questions 1 et 2 sont positives.

C'était entre C99 et C11 mais la commission ajoute :

Nous n'avons pas l'intention de réparer les règles de l'ancien style. Cependant, les observations faites dans ce document semblent être généralement correctes.

et pour autant que je puisse dire, C99 et C11 ne diffèrent pas beaucoup dans les sections que vous avez citées dans la question. Si nous examinons de plus près rapport de défaut 317 nous pouvons voir qu'il est dit :

Je crois que l'intention de C est que les définitions de fonctions à l'ancienne avec parenthèses vides ne pas donner à la fonction un type incluant un prototype pour le reste de l'unité de traduction. Par exemple :

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

Question 1 : Une telle définition de fonction donne-t-elle à la fonction un type incluant un prototype pour le reste de l'unité de traduction ?

Question 2 : L'unité de traduction ci-dessus est-elle valable ?

et la réponse du comité a été :

La réponse à la question 1 est NON, et à la question 2 est OUI. Il n'y a aucune violation de contrainte, cependant, si l'appel de fonction était exécuté il aurait un comportement indéfini. Voir 6.5.2.2;p6.

Cela semble reposer sur le fait qu'il n'est pas précisé si une définition de fonction définit un type ou un prototype, ce qui signifie qu'il n'y a pas d'exigences en matière de vérification de la compatibilité. C'était à l'origine l'intention des définitions de fonctions de l'ancien style et le comité ne clarifiera pas davantage cette question, probablement parce qu'elle est dépréciée.

Le comité souligne que ce n'est pas parce que l'unité de traduction est valide qu'il n'y a pas de comportement non défini.

0 votes

Dans le DR317, c'est clairement 1.NON, 2.OUI ; la norme spécifie clairement que void f() { } ne forme pas un prototype.

0 votes

DR316 Q2 est la même question que celle posée par ce message de l'OS ; et la résolution est que le comité pense que le code devrait être accepté (et donc par déduction, que le texte cité de 6.7.6.3 est défectueux).

0 votes

Merci, cela résout définitivement le problème, mais je suis toujours confus quant à l'"intention". Dans l'affectation "h1=f", quel est le type de h1, et quel est le type de f, et pourquoi les types de base de ces deux types de pointeurs sont-ils compatibles ? Idem si l'on ajoute "void" à la définition de f, auquel cas tout le monde (y compris les compilateurs) est d'accord pour dire que les types de base sont incompatibles.

3voto

supercat Points 25534

Historiquement, les compilateurs C géraient généralement le passage d'arguments d'une manière qui garantissait que les arguments supplémentaires seraient ignorés, et exigeaient également que les programmes ne passent des arguments que pour les paramètres qui sont effectivement utilisé ce qui permet, par exemple, de

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

pour qu'il soit possible de l'appeler en toute sécurité via foo(1,123); o foo(0); sans avoir à spécifier un second argument dans ce dernier cas. Même sur les plates-formes (par exemple, le Macintosh classique) dont la convention d'appel normale ne supporterait pas une telle garantie, les compilateurs C utilisent généralement par défaut une convention d'appel qui la supporterait.

La norme indique clairement que les compilateurs ne sont pas requis de supporter un tel usage, mais exiger des implémentations de les interdire aurait non seulement cassé le code existant, mais aurait également rendu impossible pour ces implémentations de produire un code aussi efficace que ce qui était possible en C pré-standard (puisque le code de l'application aurait dû être modifié pour passer des arguments inutiles, pour lesquels les compilateurs auraient ensuite dû générer du code). Le fait de faire de cette utilisation un comportement non défini a libéré les implémentations de toute obligation de la prendre en charge, tout en permettant aux implémentations de la prendre en charge si cela leur convient.

1voto

barak manos Points 10969

Ce n'est pas une réponse directe à votre question, mais le compilateur génère simplement l'assemblage pour pousser la valeur dans la pile avant d'appeler la fonction.

Par exemple (en utilisant le compilateur VS-2013) :

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

Si vous ajoutez une variable locale dans cette fonction, vous pouvez alors utiliser son adresse afin de trouver les valeurs que vous passez à chaque fois que vous appelez la fonction.

Par exemple (en utilisant le compilateur 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);
}

Ainsi, en substance, l'appel de la fonction avec des arguments supplémentaires est totalement sûr, car ils sont simplement poussés dans la pile avant que le compteur de programme soit placé à l'adresse de la fonction (dans la section de code de l'image exécutable).

Bien sûr, on pourrait prétendre qu'il pourrait en résulter un dépassement de pile, mais cela peut arriver dans tous les cas (même si le nombre d'arguments passés est le même que le nombre d'arguments déclarés).

-1voto

Kenneth Wilke Points 659

Pour les fonctions sans paramètres déclarés, aucun paramètre/type de paramètre n'est déduit par le compilateur. Le code suivant est essentiellement le même :

int f()
{
    return 9;
}

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

Je pense que cela a quelque chose à voir avec la façon dont les arguments de longueur variable sont supportés, et que () est fondamentalement identique à (...). En regardant de plus près le code objet généré, on constate que les arguments de f() sont toujours poussés sur les registres utilisés pour appeler la fonction, mais comme ils sont référencés dans la définition de la fonction, ils ne sont tout simplement pas utilisés dans la fonction. Si vous voulez déclarer un paramètre qui ne supporte pas les arguments, il est un peu plus correct de l'écrire comme tel :

int f(void)
{
    return 9;
}

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

Ce code ne compilera pas dans GCC pour l'erreur suivante :

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

1 votes

Non, () n'est pas fondamentalement identique à (...) bien qu'elles puissent toutes deux être mises en œuvre de la même manière. Deux fonctions, l'une définie avec () et une avec (...) L'appel d'une fonction variadique sans prototype visible a un comportement indéfini. (La plupart des compilateurs C utilisent les mêmes conventions d'appel pour les deux, pour des raisons historiques et pour satisfaire les ABIs).

0 votes

Cela n'a pas grand-chose à voir avec le sujet des pointeurs de fonction (sauf peut-être pour illustrer que la formulation pour les pointeurs de fonction ayant des types "compatibles" est différente du comportement pour appeler réellement des fonctions).

-4voto

Ivan Ivanov Points 879

J'ai essayé d'utiliser __stdcall avant la déclaration de fonction - et ça n'a pas compilé.
La raison en est que l'appel de fonction est __cdecl par défaut. Cela signifie (en plus d'autres caractéristiques) que l'appelant vide la pile après l'appel. Ainsi, la fonction appelante peut pousser sur la pile tout ce qu'elle veut, car elle sait ce qu'elle a poussé et effacera la pile de la bonne manière.
__stdcall signifie (entre autres choses) que le destinataire de l'appel nettoie la pile. Donc le nombre d'arguments doit correspondre.
... le signe indique au compilateur que le nombre d'arguments varie. Si elle est déclarée comme __stdcall, alors elle sera automatiquement remplacée par __cdecl, et vous pourrez toujours utiliser autant d'arguments que vous le souhaitez.

C'est pourquoi le compilateur avertit, mais n'arrête pas.

Exemples
Erreur : pile corrompue.

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

Travaux

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

Dans cet exemple, vous avez un comportement normal, qui n'est pas lié à la norme. Vous déclarez un pointeur vers une fonction, puis vous affectez ce pointeur, ce qui entraîne une conversion de type implicite. J'ai écrit pourquoi cela fonctionne. En c, vous pouvez également écrire

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

Et c'est toujours une partie de la norme, mais une autre partie de la norme bien sûr. Et rien ne se passe, même si vous assignez un pointeur à une fonction à un pointeur à un int.

1 votes

-1, la norme ne connaît rien de stdcall et des conventions d'appel.

0 votes

Vous avez raison. Mais quel est le problème avec la réponse ? Elle n'est pas juste ?

1 votes

Le fait est que cela ne répond pas à la question, qui pour moi est une demande d'"interprétation standard". Je ne vois pas très bien ce que vous voulez démontrer par rapport à ça, oui, si vous utilisez une extension non standard particulière, tout se casse la figure, mais la question n'était pas "est-ce que ça marche dans VC++", mais "est-ce que ça marche dans VC++". garanti pour travailler en C brut ? Ne devrait-il pas émettre des avertissements ?" Et à ces questions, une réponse faisant autorité ne peut être donnée qu'en citant la norme.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X