Des pointeurs de fonctions qui renvoient des pointeurs de tableaux... Des avis ?

Ames sensibles s'abstenir

Le problème exposé dans ce sujet a été résolu.

Bonsoir à tous,

Je suis sur le tuto C du site, et en lisant le cours sur les pointeurs de fonctions je m’aperçois que le cours passe sous silence la manière dont on coderait un pointeur sur une fonction qui retourne un pointeur sur une fonction. Pourquoi se demander cela ? A mon avis cela ne sert à rien à part à comprendre le langage C et sa philosophie, comment sont construites les syntaxes je dirais. Ca permet de mieux retenir ce qui paraît compliqué quand on ne prend pas la peine de comprendre l’idée qu’il y a derrière. N’hésitez pas à vous prêter à l’exercice avant de me relire, l’exercice est intéressant.

Bref, j’ai fait un court code et j’ai exprimé en commentaires ce que je pense que la syntaxe veut dire. Seulement je ne suis pas sûr de ce que j’écris ! Si des gens plus expérimentés que moi passent par ici je serais ravi d’avoir une confirmation (ou infirmation).

Maux de crâne garantis.


#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int (*pTab)[2];			//pointeur vers un tab de 2 int
int (**ppTab)[2];		//pointeur vers un pointeur vers un tab de 2 int
int (*(*p)[2])[2];		//pointeur vers un tableau de 2 pointeurs vers des tableaux de 2 int (oulala...)

int (*pF1) (int);		//pointeur vers une fonction (paramètre int et retour int)
int (*(*pF2)(int)) (int);	//pointeur vers une fonction qui retourne un pointeur de fonction (oulalalala...)

int (*(*pF3)(int)) [2];		//pointeur sur une fonction (paramètre int et retour pointeur sur tableau)

int (*(*(*pF4)(int)) (int)) [2];
/*pointeur sur une fonction (paramètre int et retour pointeur sur une fonction (paramètre int et retour pointeur sur tableau de 2 int)) (oulalalalalalalala)*/


int main()
{
	printf("J'ai beau etre matinal, j'ai mal au crane\n");

	return 0;
}

+0 -0

Es tu sur de tes codes ? Si tu ne les testes pas, tu peux pas être sur.

Le problème de ce genre d’exercices, c’est que c’est compliqué. Et est-ce qu’on veut du code compliqué dans nos projets ? Non, si on peut éviter. Parce que c’est un source de bugs. C’est trop facile de faire une erreur ou qu’une autre personne n’a pas compris parfaitement ces syntaxes.

Le minimum serait au moins de rendre le code plus lisible, avec des typedefs par exemple.

+4 -0

@gbdivers Je les teste mais comment m’assurer que mes commentaires sont corrects ? C’est le sens de ma question. J’ai détaillé justement que le but était de comprendre l’origine des syntaxes. Ce n’est pas un exercice censé apprendre à faire du bon code. C’est un exercice pure scolaire pour voir si on a bien compris comment on agences les syntaxes pour produire ce que l’on veut ;)

Et je ne vois pas comment je pourrais rendre mon code plus lisible justement, je ne vois pas comment il pour être aéré…

Je les teste mais comment m’assurer que mes commentaires sont corrects ?

AScriabine

En faisant simplement ce que tu écris. Si tu écris une syntaxe par exemple qui correspond selon toi à un pointeur de fonction qui prendre un tableau, alors tu peux l’utiliser concrètement.

Par exemple :

int (*pTab)[2];			//pointeur vers un tab de 2 int

int main()
{
    // on crée un tableau de 2 int
    int t[2];
    pTab = &t; // ca marche ?
}

Et je ne vois pas comment je pourrais rendre mon code plus lisible justement, je ne vois pas comment il pour être aéré…

AScriabine

En décomposant les types en types plus simples, avec des noms explicites. Par exemple :

// déclaration des types
typedef int array_of_2_ints[2];
typedef array_of_2_ints* ptr_to_array_of_2_ints;

// utilisation des types
ptr_to_array_of_2_ints pTab;
  1. Tu gagnes en lisibilité.
  2. celui qui ne maîtrise pas la syntaxe n’est pas obligé de copier-coller-modifier-merder une syntaxe complexe, il utilise juste le typedef
  3. chaque type est testable, donc si tu n’as pas le résultat voulu, tu peux regarder étape par étape la construction de ton type, plutôt que de devoir réfléchir sur la syntaxe complète complexe.

Et c’est au final ce que je n’aime pas avec ce genre d’exercice : ça pousse les gens à apprendre à réfléchir sur des sytnaxes (inutilement) complexes, plutôt que de pousser les gens à réfléchir à faire du code de qualité (= non inutilement complexe, donc moins de risque de bugs).

Il ne faut pas apprendre à comprendre les syntaxes complexes. Il faut apprendre à écrire du code simple et lisible, même pour faire des choses complexes.

+1 -0

Il ne faut pas apprendre à comprendre les syntaxes complexes. Il faut apprendre à écrire du code simple et lisible, même pour faire des choses complexes.

+1, et ça fait partie des choses les plus difficiles en programmation. Parce qu’une soupe illisible et impossible à maintenir, pratiquement tous les développeurs y arrivent.

@gbdivers et la curiosité ? La passion ? L’envie de comprendre l’origine des choses ? C’et bien aussi ! ;)

Cela dit il est évident en effet qu’une syntaxe de ce genre serait immédiatement bannie dans n’importe quel projet, et il est bon de le rappeler comme tu l’as fait

Merci pour l’aide

+1 -0

Salut,

Je suis sur le tuto C du site, et en lisant le cours sur les pointeurs de fonctions je m’aperçois que le cours passe sous silence la manière dont on coderait un pointeur sur une fonction qui retourne un pointeur sur une fonction. Pourquoi se demander cela ? A mon avis cela ne sert à rien à part à comprendre le langage C et sa philosophie, comment sont construites les syntaxes je dirais.

En plus de ce qui a été dit sur l’importance de la lisibilité du code plutôt qu’imbriquer toutes les notations comme un sauvage, des fonctions qui retournent des pointeurs sur des fonctions, c’est tout de même vachement utile pour implémenter un système de callback (et on peut les imbriquer au-fur-et-à-mesure qu’on ajoute des indirections dans le système de callback).

J’irais un peu plus loin que ce qui a été dit sur la question des typedefs, c’est pas juste utile pour rendre le code lisible, c’est aussi utile pour le rendre trivial à écrire. Une fois que tu as un typedef qui représente un pointeur sur la fonction à wrapper dans l’appel, il suffit de… réécrire la même chose pour la fonction qui wrappe la première en utilisant ce typedef comme type de retour. La question que tu poses devient alors évidente en terme de syntaxe (ce qui est une bonne chose !) parce que décomposée en briques simples connues (des bêtes pointeurs sur des fonctions), d’où le fait que le cours C n’a pas de raison particulière de détailler ça.

#gbdivers en regardant ton code de plus près je me rends compte d’une erreur que tu as commise :

int t[2];
pTab = &t; // ca marche ?

deux remarques :

  • t est un tableau et est converti automatiquement dans la plupart des cas en pointeur sur le premier élément. Cette règle a une exception ici : quand le tableau est l’opérande de &, l’expression renvoie un pointeur sur le premier élément du tableau. Donc le & est facultatif.

  • Tu cherches donc à assigner une valeur de type pointeur sur un int à un pointeur sur un tableau. Cela ne devrait pas compiler.

+0 -0

Salut,

Je suis sur le tuto C du site, et en lisant le cours sur les pointeurs de fonctions je m’aperçois que le cours passe sous silence la manière dont on coderait un pointeur sur une fonction qui retourne un pointeur sur une fonction.

AScriabine

Ce n’est effectivement pas dit explicitement, cela dit le cas d’une fonction retournant un pointeur de fonction est donné et peu servir d’exemple pour construire un tel pointeur.

/* Une fonction qui retourne un pointeur de fonction. */
void (*affiche(int a))(int);
/* On remplace l'identificateur de fonction "affiche" par un pointeur de fonction "(*p)". */
void (*(*p)(int a))(int); 

L’emploie de typedef comme souligné par @gbdrivers et @adri1 est toutefois à préconiser (rien que pour des questions de lisibilité).

Bref, j’ai fait un court code et j’ai exprimé en commentaires ce que je pense que la syntaxe veut dire. Seulement je ne suis pas sûr de ce que j’écris ! Si des gens plus expérimentés que moi passent par ici je serais ravi d’avoir une confirmation (ou infirmation).

Les exemples et description sont corrects. Le mieux pour tester est de réaliser les étapes successives et de vérifier à l’aide du compilateur. Par exemple, pour int (*(*p)[2])[2].

int a[2] = { 10, 20 }; /* Un tableau de deux int. */
int (*p)[2] = &a; /* Un pointeur sur un tableau de deux int. */
int (*b[2])[2] = { p, p }; /* Un tableau de deux pointeurs sur un tableau de deux int. */
int (*(*q)[2])[2] = &b; /* Un pointeur sur un tableau de deux pointeurs sur un tableau de deux int. */

Cela permet d’y aller progressivement et de vérifier chaque étape à l’aide du compilateur. Cela dit, aux personnes qui liront ceci : ne faites pas ça chez vous, pour votre propre bien et celui des autres. :-°

+0 -0

une erreur que tu as commise :

int t[2];
pTab = &t; // ca marche ?

AScriabine

Je n’ai pas fait d’erreur, puisque je pose justement la question de si ca marche :ange:

Bon, plus sérieusement, j’avais testé le code, il semble correct.

Sans &, j’ai l’erreur :

Testé avec clang et gcc.

Si je fais le test avec un pointeur sur int :

prog.c:7:10: warning: assignment to 'int (*)[2]' from incompatible pointer type 'int *' [-Wincompatible-pointer-types]
    7 |     pTab = t; // ca marche ?
      |          ^
prog.c:8:14: warning: initialization of 'int *' from incompatible pointer type 'int (*)[2]' [-Wincompatible-pointer-types]
    8 |     int* p = &t;
      |              ^

Donc t est bien de type int* et peut donc être assigné directement à int*, mais pas à int (*)[2]. &t est bien de type int (*)[2].

Pour les explications du pourquoi, je laisse d’autres répondre, je sais pas.

Mais d’où l’importance de tester.

+0 -0

Donc t est bien de type int* et peut donc être assigné directement à int*

Nop. t est convertible en int*. Comme c’est un tableau, il y a du decay partout et il sera affiché la plupart du temps comme un int*.

Par contre, cette transformation ne peut s’appliquer sur un pointeur de int[2] puisque le layout est différent d’un pointeur sur int*. Ce qui fait que seule la première dimension d’un tableau est affiché comme un pointeur.

Le même phénomène se rencontre aussi lorsque qu’on manipule des int[2][3]. Le compilateur aura tendance à afficher int(*)[3] (plus gcc que clang).

mémoire: [ int | int | int | int | int | ... ]
            ^       __________^
             \     /
int**: => [ int*, int*, ... ]

La mémoire n’est pas continue pour chaque élément

mémoire: [ int | int | int | int | int | ... ]
            ^_____^     ^_____^
               \          /
int(*)[2] => [ int[2], int[2], ... ]

La mémoire est continue et chaque élément fait référence à une suite de 2 int.

@Taurre Je ferais la même remarque qu’à @gbdivers : la syntaxe int (*p)[2] = &a; ne compile pas car &a désigne un pointeur sur un int et int (*p)[2] un pointeur sur un tableau ;)

AScriabine

Chez moi cela compile sans problème, j’ai testé l’exemple avant de le publier. A priori il n’y a pas de raison que cela ne fonctionne pas, comme tu l’as dit, l’emploie de l’opérateur & est une exception à la règle de la conversion implicite d’un tableau vers un pointeur sur son première élément. &tab donne donc bien un pointeur sur un tableau.

Edit : par ailleurs, à supposer que la conversion soit fausse, ce qui n’est à ma connaissance pas le cas, le compilateur émettrait un avertissement et non une erreur.

+1 -0

@Taurre y aurait-il donc une erreur dans le cours sur les tableaux ? Mes remarques se sont basées sur ce point jusqu’à présent… "Lorsqu’il est appliqué à une variable de type tableau, l’opérateur & produit comme résultat l’adresse du premier élément du tableau." ; "Dès lors, lorsque l’opérateur & est appliqué à une variable de type tableau, le résultat sera l’adresse du premier élément du tableau puisque seuls les éléments du tableau ont une adresse." Peut-être que c’est moi qui comprends rien du coup mais là je suis perdu…

D’ailleurs cette phrase me parait donc complètement fausse ? "Également, puisqu’une variable de type tableau n’est plus un pointeur, celle-ci n’a pas d’adresse. Dès lors, lorsque l’opérateur & est appliqué à une variable de type tableau, le résultat sera l’adresse du premier élément du tableau puisque seuls les éléments du tableau ont une adresse."

En lisant un autre tuto du site ("la vérité sur les tableaux et les pointeurs"), il apparaît en effet que & appliqué à un tableau renvoie un pointeur sur un tableau ce qui vient appuyer les dires des intervenants ici. Cela montre à mon sens une terrible erreur du cours principal de ce site sur les tableaux. Révolution ! D’habitude je ne corrige que des fautes d’orthographe, on dirait que j’ai mis le doigt sur quelque chose de plus intéressant :)

+0 -0

@Taurre y aurait-il donc une erreur dans le cours sur les tableaux ? Mes remarques se sont basées sur ce point jusqu’à présent… "Lorsqu’il est appliqué à une variable de type tableau, l’opérateur & produit comme résultat l’adresse du premier élément du tableau." ; "Dès lors, lorsque l’opérateur & est appliqué à une variable de type tableau, le résultat sera l’adresse du premier élément du tableau puisque seuls les éléments du tableau ont une adresse." Peut-être que c’est moi qui comprends rien du coup mais là je suis perdu…

AScriabine

Les explications du cours sont exactes. Un pointeur sur un tableau contient l’adresse du premier élément du tableau. Tu peux vérifier cela aisément à l’aide du code suivant, les trois adresses affichées sont identiques.

#include <stdio.h>

int
main(void) {
        int t[2] = { 1, 2 };
        int (*p)[2] = &t;

        printf("%p\n", (void *)t);
        printf("%p\n", (void *)&t);
        printf("%p\n", (void *)p);
        return 0;
}

Le type est différent, mais l’adresse est la même (sur le fond c’est logique, dans tous les cas on manipule un tableau, qui est une suite contiguë d’objets de même type et qui réside au même endroit en mémoire). Le typage différent permet au compilateur de calculer et d’effectuer correctement les accès, par exemple via l’opérateur [], mais c’est tout.

+0 -0

Ce qui est marrant c’est que dans plein de langages, décrire le type de fonctions qui renvoient des fonctions, c’est facile et assez naturel. En C c’est compliqué, et en plus la notion de "pointeur de fonction" ne capture pas ce qu’on veut car elle ne permet pas de faire (seule) des fermetures, qui capturent une partie de leur environnement.

Par exemple en OCaml un fonction make_counter qui renvoie un générateur de nombres uniques aura le type unit -> (unit -> int). On l’appelle avec make_counter (), et ça renvoie une fonction de type unit -> int (qui est en fait une fermeture) qui, à chaque fois qu’on l’appelle, renvoie un entier différent en partant de 0. C’est utile quand on veut décrire des objets de plusieurs catégories différentes, où on identifie les objets de chaque catégorie par des entiers consécutifs.

@Taurre C’est très intéressant Cela dit, pour moi, le cours laisse complètement penser que &tab est un pointeur sur le premier élément, donc un pointeur sur le type contenu par le tableau… Ce qui est factuellement faux puisque un pointeur ne se résume pas seulement à l’adresse pointée mais aussi au type pointé (dans le cas d’un printf avec cast en pointeur générique la différence se ressent encore moins en effet). Dire que cela revient au même relève de l’abus de langage ce qui à mon sens est à proscrire dans un cours qui s’adresse à des débutants

abus de langage ce qui à mon sens est à proscrire dans un cours qui s’adresse à des débutants

AScriabine

Sans prendre part à la discussion pour savoir si la formulation du cours est correcte ou non et s’il faut la changer, cet argument est bof. Au contraire même, enseigner, c’est rendre les choses compréhensible et c’est assez courant de faire des simplifications dans les termes utilisées, dans un premier temps.

+1 -0

@Taurre C’est très intéressant Cela dit, pour moi, le cours laisse complètement penser que &tab est un pointeur sur le premier élément, donc un pointeur sur le type contenu par le tableau… Ce qui est factuellement faux puisque un pointeur ne se résume pas seulement à l’adresse pointée mais aussi au type pointé (dans le cas d’un printf avec cast en pointeur générique la différence se ressent encore moins en effet). Dire que cela revient au même relève de l’abus de langage ce qui à mon sens est à proscrire dans un cours qui s’adresse à des débutants

AScriabine

Les deux seules choses que leur cours précise, c’est que :

  1. L’opérateur & est une exception à la conversion implicite des tableaux, elle n’a donc pas lieu ;
  2. L’opérateur &, dans le cas d’un tableau, retourne l’adresse de son premier élément.

Ces deux points sont vrais, la seule chose qui n’est pas dite, c’est que le type de l’expression retournée est un pointeur sur un tableau. Je peux ajouter une telle précision si cela semble plus clair.

+1 -0

@gbdivers Faire des simplifications est une chose, faire des erreurs en est une autre… En mathématiques par exemple, on ne dit pas au élèves de première qu’aucun nombre au carré ne donne -1, on dit que, pour les nombres réels, c’est impossible. Puis plus tard on aborde de nouveau ensembles de nombres, si bien qu’à aucun moment les cours n’ont commis de faute. Je n’ai jamais vu de ma vie un cours (un tant soit peu relu et "officiel") commettre des erreurs délibérées (et sans le préciser) dans un but de simplification. Un bon cours fait des approximations mais dans un cadre défini qui ne conduit pas son public à commettre des erreurs, sinon il y a faute pédagogique. On ne peut pas partir du principe, quand on rédige un cours, que le lecteur va lire d’autres cours plus avancés pour corriger les fautes. Maintenant, je suis pas prof ni informaticien. L’informatique soit être compliquée à enseigner au début. Aux rédacteurs de voir ce qu’il en est. Maintenant comme le précise @Taurre le cours ne dit pas explicitement que &tab est de type pointeur sur le type du tableau, donc il ne commet pas d’erreur explicite… Mais quel lecteur ne serait pas immédiatement induit dans l’erreur de le penser ? Merci pour l’aide en tout cas

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte