web-dev-qa-db-fra.com

Pourquoi est-il impossible de créer un compilateur capable de déterminer si une fonction C++ modifiera la valeur d'une variable particulière?

J'ai lu cette ligne dans un livre: 

Il est impossible de construire un compilateur qui puisse réellement détermine si une fonction C++ modifiera ou non la valeur d'un variable particulière.

Le paragraphe parlait de la raison pour laquelle le compilateur est conservateur lors de la vérification de const-ness. 

Pourquoi est-il impossible de construire un tel compilateur?  

Le compilateur peut toujours vérifier si une variable est réaffectée, si une fonction non const est appelée ou si elle est transmise en tant que paramètre non const ...

102
Cricketer

Pourquoi est-il impossible de construire un tel compilateur?

Pour la même raison, vous ne pouvez pas écrire un programme qui déterminera si un programme donné va se terminer. Ceci est connu sous le nom de problème d’arrêt , et c’est l’une de ces choses non calculable.

Pour être clair, vous pouvez écrire un compilateur qui peut déterminer qu’une fonction modifie la variable dans certains cas, mais vous ne pouvez pas en écrire une qui vous indique de manière fiable que la fonction modifiera ou ne modifiera pas la variable ( ou arrêter) pour chaque fonction possible.

Voici un exemple simple:

void foo() {
    if (bar() == 0) this->a = 1;
}

Comment un compilateur peut-il déterminer, à partir de ce code, si foo changera un jour a? Que cela dépende ou non des conditions externes à la fonction, à savoir l'implémentation de bar. Il y a bien plus que cela à prouver que le problème posé n'est pas calculable, mais il est déjà expliqué en détail dans l'article lié à Wikipedia (et dans tous les manuels de théorie du calcul), je ne tenterai donc pas de l'expliquer correctement ici.

136
Caleb

Imaginez qu'un tel compilateur existe. Supposons également que, par souci de commodité, elle fournisse une fonction de bibliothèque qui renvoie 1 si la fonction transmise modifie une variable donnée et 0 si la fonction ne le fait pas. Alors que doit imprimer ce programme?

int variable = 0;

void f() {
    if (modifies_variable(f, variable)) {
        /* do nothing */
    } else {
        /* modify variable */
        variable = 1;
    }
}

int main(int argc, char **argv) {
    if (modifies_variable(f, variable)) {
        printf("Modifies variable\n");
    } else {
        printf("Does not modify variable\n");
    }

    return 0;
}
121
orlp

Ne confondez pas "modifiera ou ne modifiera pas une variable étant donné ces entrées" pour "a un chemin d'exécution qui modifie une variable."

Le premier s'appelle détermination du prédicat opaque , et il est trivialement impossible de décider - mis à part la réduction du problème, vous pouvez simplement indiquer que les entrées peuvent provenir d'une source inconnue (par exemple, l'utilisateur). Cela est vrai de tous les langages, pas seulement du C++.

Cette dernière déclaration, cependant, peut être déterminée en regardant l’arbre d’analyse syntaxique, ce que tous les compilateurs optimistes font. La raison en est que les fonctions pures(et référentiellement transparent fonctions, pour certaines définitions de référentiellement transparent ) ont toutes sortes d’optimisations de Nice qui peuvent facilement inclinable ou dont les valeurs sont déterminées au moment de la compilation; mais pour savoir si une fonction est pure, il faut savoir si peut modifier une variable.

Ainsi, ce qui semble être une déclaration surprenante à propos de C++ est en réalité une déclaration triviale à propos de toutes les langues.

Je pense que le mot clé dans "si une fonction C++ va changer la valeur d'une variable particulière" est "sera". Il est certainement possible de construire un compilateur qui vérifie si une fonction C++ est autorisée à changer la valeur d'une variable particulière, vous ne pouvez pas dire avec certitude que le changement va se produire:

void maybe(int& val) {
    cout << "Should I change value? [Y/N] >";
    string reply;
    cin >> reply;
    if (reply == "Y") {
        val = 42;
    }
}
28
dasblinkenlight

Je ne pense pas qu'il soit nécessaire d'invoquer le problème bloquant pour expliquer que vous ne pouvez pas savoir par un algorithme lors de la compilation si une fonction donnée modifiera une variable ou non.

Au lieu de cela, il suffit de souligner que le comportement d'une fonction dépend souvent de conditions d'exécution que le compilateur ne peut pas connaître à l'avance. Par exemple.

int y;

int main(int argc, char *argv[]) {
   if (argc > 2) y++;
}

Comment le compilateur peut-il prédire avec certitude si y sera modifié?

15
LarsH

Cela peut être fait et les compilateurs le font tout le temps pour certaines fonctions , il s’agit par exemple d’une optimisation triviale pour de simples accesseurs en ligne ou de nombreuses fonctions pures. 

Ce qui est impossible, c'est de le savoir dans le cas général.

Chaque fois qu'il y a un appel système ou un appel de fonction provenant d'un autre module, ou un appel à une méthode potentiellement surchargée, tout peut arriver, y compris une prise de contrôle hostile de l'utilisation par un pirate d'un débordement de pile pour modifier une variable non liée.

Cependant, vous devez utiliser const, éviter les globales, préférer les références aux pointeurs, éviter de réutiliser des variables pour des tâches non liées, etc., ce qui facilitera la vie du compilateur lorsqu’on procédera à des optimisations agressives.

7
kriss

Il existe plusieurs voies pour expliquer cela, dont le problème Halting :

Dans la théorie de la calculabilité, le problème d’arrêt peut être énoncé comme suit: "À partir d’une description d’un programme informatique arbitraire, décidez si le programme se termine ou continue à courir pour toujours". Cela équivaut au problème de décider, à partir d'un programme et d'une entrée, si le programme s'arrêtera éventuellement lorsqu'il sera exécuté avec cette entrée ou s'il fonctionnera indéfiniment.

Alan Turing prouva en 1936 qu’il n’existait pas d’algorithme général permettant de résoudre le problème d’arrêt de toutes les paires d’entrées de programme possibles.

Si j'écris un programme qui ressemble à ceci:

do tons of complex stuff
if (condition on result of complex stuff)
{
    change value of x
}
else
{
    do not change value of x
}

La valeur de x change-t-elle? Pour le déterminer, vous devez d’abord déterminer si la partie do tons of complex stuff provoque le déclenchement de la condition - ou même plus élémentaire, si elle s’arrête. C'est quelque chose que le compilateur ne peut pas faire.

6
Timothy Shields

Vraiment surpris qu'il n'y ait pas de réponse qui utilise directement le problème d'arrêt! Il y a une réduction très simple de ce problème au problème bloquant. 

Imaginons que le compilateur puisse dire si une fonction modifie ou non la valeur d'une variable. Ensuite, il serait certainement en mesure de dire si la fonction suivante modifie la valeur de y ou non, en supposant que la valeur de x puisse être suivie dans tous les appels dans le reste du programme:

foo(int x){
   if(x)
       y=1;
}

Maintenant, pour tout programme que nous aimons, réécrivons comme suit:

int y;
main(){
    int x;
    ...
    run the program normally
    ...
    foo(x);
}

Notez que si et seulement si notre programme change la valeur de y, il se termine alors - foo () est la dernière chose à faire avant de quitter. Cela signifie que nous avons résolu le problème d'arrêt!

La réduction ci-dessus nous montre que le problème consistant à déterminer si la valeur d'une variable change est au moins aussi difficile que le problème stoppant. Le problème d’arrêt est connu pour être incomputable, donc celui-ci doit l'être aussi.

6
John Doucette

Dès qu'une fonction appelle une autre fonction dont le compilateur ne "voit" pas la source, elle doit soit supposer que la variable a été modifiée, sinon les choses risquent de mal tourner plus bas. Par exemple, disons que nous avons ceci dans "foo.cpp":

 void foo(int& x)
 {
    ifstream f("f.dat", ifstream::binary);
    f.read((char *)&x, sizeof(x));
 }

et nous avons ceci dans "bar.cpp":

void bar(int& x)
{
  foo(x);
}

Comment le compilateur peut-il "savoir" que x ne change pas (ou IS ne change pas, de manière plus appropriée) dans bar?

Je suis sûr que nous pouvons proposer quelque chose de plus complexe, si ce n'est pas assez complexe.

4
Mats Petersson

Il est en général impossible pour le compilateur de déterminer si la variable sera sera modifiée, comme cela a été souligné.

Lors de la vérification de la constance, la question qui nous intéresse semble être de savoir si la variable can / peut être modifiée par une fonction. Même cela est difficile dans les langues qui supportent les pointeurs. Vous ne pouvez pas contrôler ce que fait un autre code avec un pointeur, il pourrait même être lu depuis une source externe (bien que cela soit peu probable). Dans les langages qui restreignent l'accès à la mémoire, ces types de garanties peuvent être possibles et permettent une optimisation plus agressive que le C++.

1
Krumelur

Pour rendre la question plus précise, je suggère que l’ensemble de contraintes suivant ait pu être ce que l’auteur du livre avait peut-être en tête:

  1. Supposons que le compilateur examine le comportement d'une fonction spécifique par rapport à la constance d'une variable. Pour être correct, un compilateur devrait assumer (en raison du repliement de l'alias comme expliqué ci-dessous) si la fonction appelée autre fonction de la variable est modifiée, donc l'hypothèse n ° 1 ne s'applique qu'aux fragments de code qui ne font pas d'appels de fonction.
  2. Supposons que la variable ne soit pas modifiée par une activité asynchrone ou simultanée.
  3. Supposons que le compilateur détermine uniquement si la variable peut être modifiée, et non si elle le sera. En d'autres termes, le compilateur effectue uniquement une analyse statique.
  4. Supposons que le compilateur considère uniquement le code qui fonctionne correctement (sans tenir compte des dépassements/insuffisances de tableaux, des pointeurs incorrects, etc.)

Dans le contexte de la conception du compilateur, je pense que les hypothèses 1, 3 et 4 sont parfaitement logiques dans l’optique d’un rédacteur de compilateur dans le contexte de l’exactitude du code et/ou de l’optimisation du code. L'hypothèse 2 est logique en l'absence du mot clé volatile. Et ces hypothèses concentrent également la question suffisamment pour rendre le jugement d'une réponse proposée beaucoup plus définitif :-) 

Compte tenu de ces hypothèses, l'une des principales raisons pour lesquelles la const-ness ne peut pas être supposée est due à un aliasing variable. Le compilateur ne peut pas savoir si une autre variable pointe sur la variable const. Le crénelage peut être dû à une autre fonction de la même unité de compilation, auquel cas le compilateur pourrait examiner plusieurs fonctions et utiliser un arbre d'appels pour déterminer de manière statique qu'un crénelage pourrait se produire. Mais si l'alias est dû à une bibliothèque ou à un autre code étranger, le compilateur n'a aucun moyen de savoir lors de l'entrée de la fonction si les variables ont un alias.

Vous pourriez faire valoir que si une variable/argument est marqué const, il ne devrait pas être possible de le modifier via un aliasing, mais pour un écrivain du compilateur, c'est assez risqué. Il peut même être risqué pour un programmeur humain de déclarer une variable const dans le cadre, par exemple, d’un grand projet dans lequel il ne connaît pas le comportement de tout le système, ni du système d’exploitation, ni d’une bibliothèque, de connaître réellement une variable gagnée. t changer.

1
Χpẘ

Même si une variable est déclarée const, cela ne signifie pas qu'un code mal écrit peut l'écraser.

//   g++ -o foo foo.cc

#include <iostream>
void const_func(const int&a, int* b)
{
   b[0] = 2;
   b[1] = 2;
}

int main() {
   int a = 1;
   int b = 3;

   std::cout << a << std::endl;
   const_func(a,&b);
   std::cout << a << std::endl;
}

sortie:

1
2
0
Mark Lakata

Pour développer mes commentaires, le texte de ce livre ne dit pas ce qui obscurcit la question.

Comme je l'ai commenté, ce livre tente de dire: "demandons à un nombre infini de singes d'écrire toutes les fonctions C++ imaginables qui pourraient être écrites. Il y aura des cas où si nous sélectionnons une variable qui (une fonction particulière écrite par les singes) utilise, nous ne pouvons pas savoir si la fonction changera cette variable ".

Bien sûr, pour certaines (voire beaucoup) de fonctions dans une application donnée, cela peut être déterminé très facilement par le compilateur. Mais pas pour tous (ni nécessairement pour la plupart).

Cette fonction peut être facilement analysée:

static int global;

void foo()
{
}

"foo" ne modifie évidemment pas "global". Cela ne modifie rien du tout et un compilateur peut résoudre cela très facilement.

Cette fonction ne peut pas être ainsi analysée:

static int global;

int foo()
{
    if ((Rand() % 100) > 50)
    {
        global = 1;
    }
    return 1;

Etant donné que les actions de "foo" dépendent d'une valeur qui peut changer au moment de l'exécution , elle ne peut évidemment pas être déterminée au moment de la compilation si elle modifiera le mot "global".

Ce concept est beaucoup plus simple à comprendre que ne le prétendent les informaticiens. Si la fonction peut faire quelque chose de différent en fonction des choses qui peuvent changer au moment de l'exécution, vous ne pouvez pas savoir ce qu'elle va faire jusqu'à ce qu'elle soit exécutée et chaque fois qu'elle sera exécutée, elle pourra faire quelque chose de différent. Que ce soit impossible ou non, c'est évidemment impossible.

0
El Zorko