mardi 6 janvier 2015

Arithmétique des pointeurs...et fonctions...

Dans mon éternelle quête de l'inépuisable Graal, je viens de découvrir (naïf que je suis) que l'arithmétique des pointeurs révèle des surprises. Je passe outre le fait que les débordements de tableaux via les pointeurs sont des comportements plus étranges qu'il n'y paraît. Le standard indique à propos de la sémantique de l'arithmétique:
Section 6.5.6/8 Additive operators. When an expression that has integer type is added to or subtracted from a pointer, the result has the type of the pointer operand. If the pointer operand points to an element of an array object, and the array is large enough, the result points to an element offset from the original element such that the difference of the subscripts of the resulting and original array elements equals the integer expression. In other words, if the expression P points to the i-th element of an array object, the expressions (P)+N (equivalently, N+(P)) and (P)-N (where N has the value n) point to, respectively, the i+n-th and i−n-th elements of the array object, provided they exist. Moreover, if the expression P points to the last element of an array object, the expression (P)+1 points one past the last element of the array object, and if the expression Q points one past the last element of an array object, the expression (Q)-1 points to the last element of the array object. If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined. If the result points one past the last element of the array object, it shall not be used as the operand of a unary * operator that is evaluated. 
Oui j'ai bien lu (pas vous ?): If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behavior is undefined. Ce qui signifie en clair que l'on peut pointer à l'adresse qui suit immédiatement le dernier élément d'un tableau et que l'adresse stockée dans le pointeur est quelque chose une véritable adresse correctement calculée quand bien même on ne doit pas déréférencer le pointeur (c'est le end des itérateurs C++); mais pointer plus loin est un comportement non défini. Autrement dit, un programme faisant une telle opération ne peut être considéré comme juste, ce n'est pas un programme C! Attention c'est non-défini pas non-spécifié, par conséquent le compilateur/le runtime sont libres de faire ce qu'il veulent (y compris réinstaller Windows par-dessus votre cher Linux rien que pour vous punir).

Bon mais ma question originelle était le problème de l'arithmétique sur les pointeurs de fonction! Soit l'exemple suivant :
void f() {
  // super code génial
}
...
void (*pf)();
pf = f;
pf();
// Ok je vois bien
pf++;
pf(); // Argh!
...
Dans ce cas, il est évident que le second appel serait bien étrange, mais prenons l'exemple suivant:
void f() {
  // mega code trop bien
}
void g() {
  // hyper code trop cool
}
void magic(int i) {
  (f+(g-f)*i)();
}
L'idée étant que si on appelle magic(0) c'est f qui s'exécute et si on appelle magic(1) c'est g. Une compilation ordinaire ne dit rien et le code fonctionne. Si l'on active le standard c99, rien non plus. Par contre le mode pédant :
$ gcc --std=c99 -pedantic -c t.c
t.c:10:8: warning: arithmetic on pointers to the function type 'void ()' is a
      GNU extension [-Wpointer-arith]
  (f+(g-f)*i)();
      ~^~
t.c:10:5: warning: arithmetic on a pointer to the function type 'void ()' is a
      GNU extension [-Wpointer-arith]
  (f+(g-f)*i)();
Argh, l'arithmétique sur les pointeurs de fonctions est donc encore plus limitée ? Voyons ce qu'en dit le standard alors :
6.5.6/3 Additive operators. For subtraction, one of the following shall hold:
—  both operands have arithmetic type;
—  both operands are pointers to qualified or unqualified versions of compatible complete object types; or
—  the left operand is a pointer to a complete object type and the right operand has integer type.
What is object type ??? Appelons le standard de Cognacq-Jay:
6.2.5/1 Types. The meaning of a value stored in an object or returned by a function is determined by the type of the expression used to access it. (An identifier declared to be an object is the simplest such expression; the type is specified in the declaration of the identifier.) Types are partitioned into object types (types that describe objects) and function types (types that describe functions). At various points within a translation unit an object type may be incomplete (lacking sufficient information to determine the size of objects of that type) or complete (having sufficient information).
Bon ben ok, ben les pointers to functions types n'ont donc pas d'arithmétique! Ben voilà... Je suis déçu, encore une fois j'ai la preuve que je ne possède pas le Craal.

DjiBee

3 commentaires:

  1. Euh, tu attendrais quoi pour magic(2) ?
    :-)

    RépondreSupprimer
  2. Humm, je dirais un accès direct au kernel ?

    RépondreSupprimer
  3. Aldebaraaaaaaan7 mars 2015 à 16:45

    Cet article vient de me sauver la vie. Je ne comprenais pas pourquoi, sur mon code Linkit One (quasi arduino due), lorsque j'utilisais des pointeurs sur des tableaux de char, un printf pouvais m'afficher les chaines suivante en mémoire.
    Je résolvais le soucis en ajoutant un '\0' à la fin de mon tableau mais je crois qu'il serait plus judicieux de me passer des pointer sur ce coup. Merci pour ce billet pédagogique !

    RépondreSupprimer