Comment fonctionne le code C qui imprime de 1 à 1000 sans boucles ni instructions conditionnelles?

148

J'ai trouvé du Ccode qui imprime de 1 à 1000 sans boucles ni conditionnelles : mais je ne comprends pas comment cela fonctionne. Quelqu'un peut-il parcourir le code et expliquer chaque ligne?

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

void main(int j) {
  printf("%d\n", j);
  (&main + (&exit - &main)*(j/1000))(j+1);
}
ob_dev
la source
1
Compilez-vous en C ou en C ++? Quelles erreurs voyez-vous? Vous ne pouvez pas appeler mainen C ++.
ninjalj
@ninjalj J'ai créé un projet C ++ et copiez / collez le code, l'erreur est: illégale, l'opérande gauche a le type 'void (__cdecl *) (int)' et l'expression doit être un pointeur vers un type d'objet complet
ob_dev
1
@ninjalj Ce code fonctionne sur ideone.org mais pas dans le studio visuel ideone.com/MtJ1M
ob_dev
@oussama Similaire, mais un peu plus difficile à comprendre: ideone.com/2ItXm De rien . :)
Marquez
2
j'ai supprimé tous les caractères '&' de ces lignes (& main + (& exit - & main) * (j / 1000)) (j + 1); et ce code fonctionne toujours.
ob_dev

Réponses:

264

N'écrivez jamais de code comme ça.


Pour j<1000, j/1000vaut zéro (division entière). Alors:

(&main + (&exit - &main)*(j/1000))(j+1);

est équivalent à:

(&main + (&exit - &main)*0)(j+1);

Lequel est:

(&main)(j+1);

Qui appelle mainavec j+1.

Si j == 1000, alors les mêmes lignes sortent comme:

(&main + (&exit - &main)*1)(j+1);

Ce qui se résume à

(&exit)(j+1);

Ce qui est exit(j+1)et quitte le programme.


(&exit)(j+1)et exit(j+1)sont essentiellement la même chose - citant C99 §6.3.2.1 / 4:

Un désignateur de fonction est une expression qui a un type de fonction. Sauf s'il s'agit de l'opérande de l'opérateur sizeof ou de l' opérateur unaire & , un désignateur de fonction de type " type de retour de fonction " est converti en une expression de type " pointeur vers type de retour de fonction ".

exitest un désignateur de fonction. Même sans l'opérateur d' &adresse unaire de, il est traité comme un pointeur vers une fonction. (Le &juste le rend explicite.)

Et les appels de fonction sont décrits au §6.5.2.2 / 1 et suivants:

L'expression qui désigne la fonction appelée doit avoir un pointeur de type sur la fonction renvoyant void ou renvoyant un type d'objet autre qu'un type tableau.

Cela exit(j+1)fonctionne à cause de la conversion automatique du type de fonction en type pointeur vers fonction, et (&exit)(j+1)fonctionne également avec une conversion explicite en type pointeur vers fonction.

Cela étant dit, le code ci-dessus n'est pas conforme ( mainprend soit deux arguments, soit aucun), et &exit - &mainest, je crois, indéfini selon le §6.5.6 / 9:

Lorsque deux pointeurs sont soustraits, les deux doivent pointer vers des éléments du même objet tableau , ou un après le dernier élément de l'objet tableau; ...

L'addition (&main + ...)serait valide en soi, et pourrait être utilisée, si la quantité ajoutée était nulle, puisque le §6.5.6 / 7 dit:

Pour les besoins de ces opérateurs, un pointeur vers un objet qui n'est pas un élément d'un tableau se comporte de la même manière qu'un pointeur vers le premier élément d'un tableau de longueur un avec le type de l'objet comme type d'élément.

Donc, ajouter zéro à ce &mainserait correct (mais pas très utile).

Tapis
la source
4
foo(arg)et (&foo)(arg)sont équivalents, ils appellent foo avec l'argument arg. newty.de/fpt/fpt.html est une page intéressante sur les pointeurs de fonction.
Mat
1
@Krishnabhadra: dans le premier cas, fooest un pointeur, &fooest l'adresse de ce pointeur. Dans le second cas, fooest un tableau et &fooéquivaut à foo.
Mat
8
Inutilement complexe, du moins pour C99:((void(*[])()){main, exit})[j / 1000](j + 1);
Per Johansson
1
&foon'est pas la même chose que foolorsqu'il s'agit d'un tableau. &fooest un pointeur vers le tableau, fooest un pointeur vers le premier élément. Ils ont cependant la même valeur. Pour les fonctions, funet &funsont tous deux des pointeurs vers la fonction.
Per Johansson
1
Pour votre information, si vous regardez la réponse pertinente à l'autre question citée ci-dessus , vous verrez qu'il existe une variante qui est en fait conforme à C99. Effrayant, mais vrai.
Daniel Pryden
41

Il utilise la récursivité, l'arithmétique des pointeurs et exploite le comportement d'arrondi de la division entière.

Le j/1000terme s'arrondit à 0 pour tous j < 1000; une fois jatteint 1000, il est évalué à 1.

Maintenant, si vous avez a + (b - a) * n, où nest 0 ou 1, vous vous retrouvez avec asi n == 0et bsi n == 1. En utilisant &main(l'adresse de main()) et &exitpour aet b, le terme (&main + (&exit - &main) * (j/1000))renvoie &mainlorsque jest inférieur à 1000, &exitsinon. Le pointeur de fonction résultant reçoit ensuite l'argument j+1.

L'ensemble de cette construction entraîne un comportement récursif: tant qu'il jest inférieur à 1000, il mains'appelle lui-même de manière récursive; lorsqu'il jatteint 1000, il appelle à la exitplace, faisant quitter le programme avec le code de sortie 1001 (ce qui est un peu sale, mais fonctionne).

tdammers
la source
1
Bonne réponse, mais un doute .. Comment la sortie principale avec le code de sortie 1001? Main ne renvoie rien .. Une valeur de retour par défaut?
Krishnabhadra
2
Lorsque j atteint 1000, main ne rentre plus en lui-même; à la place, il appelle la fonction libc exit, qui prend le code de sortie comme argument et, eh bien, quitte le processus en cours. À ce stade, j vaut 1000, donc j + 1 vaut 1001, ce qui devient le code de sortie.
tdammers