Perché è impossibile creare un compilatore in grado di determinare se una funzione C ++ cambierà il valore di una particolare variabile?


104

Ho letto questa riga in un libro:

È dimostrato che è impossibile costruire un compilatore che possa effettivamente determinare se una funzione C ++ cambierà o meno il valore di una particolare variabile.

Il paragrafo parlava del motivo per cui il compilatore è conservatore quando controlla const-ness.

Perché è impossibile costruire un tale compilatore?

Il compilatore può sempre controllare se una variabile viene riassegnata, una funzione non const viene invocata su di essa o se viene passata come parametro non const ...


24
La prima cosa che mi viene in mente sono le librerie di collegamento dinamico. Se compilo codice sulla mia macchina e tu compili codice sulla tua macchina e li colleghiamo in fase di esecuzione , come potrebbe il tuo compilatore sapere se ho modificato le variabili o no?
Mooing Duck

4
@MooingDuck Esattamente questo. Più in generale, il compilatore non compila la funzione individualmente, ma la compila come parte di un'immagine più ampia che potrebbe non essere tutta nell'ambito del compilatore.
named2voyage

3
"impossibile" può essere un'esagerazione - "computazionalmente non fattibile" (come in NP-hard) può essere una caratterizzazione migliore, ma è un po 'più difficile da comprendere per lo studente. Immagina un elenco collegato o un'altra struttura di dati astratta. Se chiamo una funzione che cambia un nodo in quella lista / albero / qualunque cosa, come potrebbe mai un compilatore sperare di dimostrare esattamente quale nodo è stato modificato (e forse ancora più importante, quale no) senza fondamentalmente simulare completamente il programma con il input previsto, il tutto senza
impiegare

36
@twalberg Impossible non è un'esagerazione, il problema dell'arresto si applica qui come spiegano diverse risposte. Semplicemente non è possibile analizzare algoritmicamente completamente un programma generale.
Fiktik

5
@twalberg I compilatori che compilano solo un sottoinsieme di programmi validi non sono molto utili.
Caleb

Risposte:


139

Perché è impossibile costruire un tale compilatore?

Per lo stesso motivo per cui non è possibile scrivere un programma che determinerà se un determinato programma verrà terminato. Questo è noto come il problema dell'arresto ed è una di quelle cose che non è calcolabile.

Per essere chiari, puoi scrivere un compilatore che possa determinare che una funzione cambia la variabile in alcuni casi , ma non puoi scriverne uno che ti dica in modo affidabile che la funzione cambierà o non cambierà la variabile (o si fermerà) per ogni possibile funzione.

Ecco un semplice esempio:

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

Come può un compilatore determinare, solo guardando quel codice, se foocambierà mai a? Che lo faccia o no dipende da condizioni esterne alla funzione, vale a dire l'implementazione di bar. C'è molto di più nella prova che il problema dell'arresto non è calcolabile, ma è già ben spiegato nell'articolo di Wikipedia collegato (e in ogni libro di testo di teoria del calcolo), quindi non cercherò di spiegarlo correttamente qui.


48
@mrsoltys, i computer quantistici sono "solo" esponenzialmente più veloci per alcuni problemi, non possono risolvere problemi indecidibili.
zch

8
@mrsoltys Questi algoritmi esponenzialmente complicati (come il factoring) sono perfetti per i computer quantistici, ma arrestare il problema è un dilemma logico, non è calcolabile indipendentemente dal tipo di "computer" che hai.
user1032613

7
@ mrsoltys, solo per essere un furbo, sì, cambierebbe. Sfortunatamente, significherebbe che l'algoritmo è sia terminato che ancora in esecuzione, sfortunatamente, non puoi dire quale senza osservare direttamente, con cui influisci sullo stato attuale.
Nathan Ernst

9
@ ThorbjørnRavnAndersen: OK, quindi supponiamo che io stia eseguendo un programma. Come determino esattamente se terminerà?
ruakh

8
@ ThorbjørnRavnAndersen Ma se esegui effettivamente il programma e non termina (ad esempio un ciclo infinito), non scoprirai mai che non termina ... continui semplicemente a eseguire un altro passaggio, perché potrebbe essere l'ultimo ...
MaxAxeHax

124

Immagina che esista un tale compilatore. Supponiamo anche che per comodità fornisca una funzione di libreria che restituisce 1 se la funzione passata modifica una data variabile e 0 quando la funzione non lo fa. Allora cosa dovrebbe stampare questo programma?

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

12
Bello! Il paradosso sono un bugiardo come scritto da un programmatore.
Krumelur

28
In realtà è solo un bell'adattamento della famosa prova dell'indecidibilità del problema dell'arresto .
Konstantin Weitz

10
In questo caso concreto, "modifica_variabile" dovrebbe restituire true: C'è almeno un percorso di esecuzione in cui la variabile viene effettivamente modificata. E quel percorso di esecuzione viene raggiunto dopo una chiamata a una funzione esterna, non deterministica, quindi l'intera funzione è non deterministica. Per questi 2 motivi, il compilatore dovrebbe prendere la vista irrisoria e decidere di modificare la variabile. Se il percorso per modificare la variabile viene raggiunto dopo un confronto deterministico (verificabile dal compilatore) restituisce falso (cioè "1 == 1"), allora il compilatore potrebbe tranquillamente dire che tale funzione non modifica mai la variabile
Joe Pineda

6
@ JoePineda: La domanda è se fmodifica la variabile, non se può modificare la variabile. Questa risposta è corretta.
Neil G

4
@ JoePineda: nulla mi impedisce di copiare / incollare il codice di modifies_variabledal sorgente del compilatore, annullando totalmente il tuo argomento. (supponendo open-source, ma il punto dovrebbe essere chiaro)
orlp

60

Non confondere "modificherà o non modificherà una variabile dati questi input" perché "ha un percorso di esecuzione che modifica una variabile".

Il primo è chiamato determinazione del predicato opaco ed è banalmente impossibile da decidere: a parte la riduzione del problema dell'arresto, potresti semplicemente sottolineare che gli input potrebbero provenire da una fonte sconosciuta (ad es. L'utente). Questo è vero per tutti i linguaggi, non solo per il C ++.

Quest'ultima affermazione, tuttavia, può essere determinata guardando l'albero di analisi, che è qualcosa che fanno tutti i compilatori di ottimizzazione. Il motivo per cui lo fanno è che le funzioni pure (e le funzioni referenzialmente trasparenti , per qualche definizione di referenzialmente trasparente ) hanno tutti i tipi di belle ottimizzazioni che possono essere applicate, come essere facilmente inlinabili o avere i loro valori determinati in fase di compilazione; ma per sapere se una funzione è pura, dobbiamo sapere se può mai modificare una variabile.

Quindi, quella che sembra essere un'affermazione sorprendente sul C ++ è in realtà un'affermazione banale su tutti i linguaggi.


5
Questa è la migliore risposta imho, è importante fare questa distinzione.
UncleZeiv

"banalmente impossibile"?
Kip

2
@Kip "banalmente impossibile da decidere" probabilmente significa "impossibile da decidere, e la prova è banale".
fredoverflow

28

Penso che la parola chiave in "se una funzione C ++ cambierà o meno il valore di una particolare variabile" è "volontà". È certamente possibile costruire un compilatore che controlli se una funzione C ++ è autorizzata o meno a modificare il valore di una particolare variabile, non si può dire con certezza che il cambiamento avverrà:

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

"È certamente possibile creare un compilatore che controlli se una funzione C ++ può modificare o meno il valore di una determinata variabile" No, non lo è. Vedi la risposta di Caleb. Affinché un compilatore sappia se foo () può modificare a, dovrebbe sapere se è possibile che bar () restituisca 0. E non esiste una funzione calcolabile che possa indicare tutti i possibili valori di ritorno di qualsiasi funzione calcolabile. Quindi esistono percorsi di codice tali che il compilatore non sarà in grado di dire se verranno mai raggiunti. Se una variabile viene modificata solo in un percorso di codice che non può essere raggiunto, non cambierà, ma un compilatore non la rileverà
Martin Epsz

12
@ MartinEpsz Con "posso" intendo "è consentito cambiare", non "può eventualmente cambiare". Credo che questo fosse ciò che OP aveva in mente quando parlava di constcontrolli dello stato di salute.
dasblinkenlight

@dasblinkenlight dovrei essere d'accordo sul fatto che credo che l'OP potrebbe aver significato il primo, "è consentito o cambiare", o "può o non può cambiare" vs. "sicuramente non cambierà". Ovviamente non riesco a pensare a uno scenario in cui questo potrebbe essere un problema. Potresti anche modificare il compilatore per rispondere semplicemente "può cambiare" su qualsiasi funzione contenente l'identificatore o una chiamata a una funzione che ha un attributo di risposta "può cambiare". Detto questo, C e C ++ sono linguaggi orribili con cui provare questo, dal momento che hanno una definizione così ampia delle cose. Penso che questo sia il motivo per cui const-ness sarebbe un problema in C ++.
DDS

@ MartinEpsz: "E non esiste una funzione calcolabile che possa indicare tutti i possibili valori di ritorno di qualsiasi funzione calcolabile". Penso che il controllo di "tutti i possibili valori di ritorno" sia un approccio errato. Esistono sistemi matematici (maxima, mathlab) che possono risolvere equazioni, il che significa che avrebbe senso applicare un approccio simile alle funzioni. Cioè trattalo come un'equazione con diverse incognite. I problemi sono il controllo del flusso + effetti collaterali => situazioni irrisolvibili. IMO, senza quelli (linguaggio funzionale, nessun compito / effetti collaterali), sarebbe stato possibile prevedere quale percorso prenderà il programma
SigTerm

16

Non penso sia necessario invocare il problema dell'arresto per spiegare che non è possibile sapere algoritmicamente in fase di compilazione se una data funzione modificherà o meno una determinata variabile.

È invece sufficiente sottolineare che il comportamento di una funzione dipende spesso dalle condizioni di runtime, che il compilatore non può conoscere in anticipo. Per esempio

int y;

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

Come potrebbe il compilatore prevedere con certezza se yverrà modificato?


7

Può essere fatto ei compilatori lo fanno continuamente per alcune funzioni , questa è ad esempio una banale ottimizzazione per semplici funzioni di accesso in linea o molte funzioni pure.

Ciò che è impossibile è saperlo nel caso generale.

Ogni volta che c'è una chiamata di sistema o una chiamata di funzione proveniente da un altro modulo, o una chiamata a un metodo potenzialmente sovrascritto, potrebbe accadere qualsiasi cosa, inclusa l'acquisizione ostile dall'uso di uno stack overflow da parte di alcuni hacker per modificare una variabile non correlata.

Tuttavia dovresti usare const, evitare le globali, preferire i riferimenti ai puntatori, evitare di riutilizzare le variabili per attività non correlate, ecc. Che renderà più facile la vita del compilatore quando si eseguono ottimizzazioni aggressive.


1
Se lo ricordo correttamente, questo è il punto centrale della programmazione funzionale, giusto? Usando solo funzioni puramente deterministiche e senza effetti collaterali, i compilatori sono liberi di eseguire ottimizzazioni aggressive, pre-esecuzione, post-esecuzione, memoizzazione e persino esecuzione in fase di compilazione. Il punto che penso che molti dei rispondenti ignorino (o siano confusi) è che è effettivamente possibile per un sottoinsieme ben educato di tutti i programmi . E no, questo sottoinsieme non è banale o poco interessante, in realtà è molto utile. Ma è davvero impossibile per il caso generale assoluto.
Joe Pineda

Il sovraccarico è un concetto in fase di compilazione. Probabilmente intendevi "metodo sovrascritto".
fredoverflow

@FredOverflow: sì, intendo overriden. Il sovraccarico è davvero un concetto in fase di compilazione. Grazie per averlo individuato (ovviamente se l'implementazione proviene da un'altra unità di compilazione, il compilatore può ancora avere problemi ad analizzarlo, ma non era quello che intendevo). Fisserò la risposta.
kriss

6

Ci sono molti modi per spiegarlo, uno dei quali è il problema dell'arresto :

Nella teoria della computabilità, il problema dell'arresto può essere affermato come segue: "Data una descrizione di un programma per computer arbitrario, decidere se il programma termina l'esecuzione o continua a funzionare all'infinito". Ciò equivale al problema di decidere, dato un programma e un input, se il programma alla fine si fermerà quando viene eseguito con quell'input o se verrà eseguito per sempre.

Alan Turing ha dimostrato nel 1936 che non può esistere un algoritmo generale per risolvere il problema dell'arresto per tutte le possibili coppie di input di programma.

Se scrivo un programma simile a questo:

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

Il valore del xcambiamento? Per determinarlo, dovresti prima determinare se la do tons of complex stuffparte causa l' attivazione della condizione o, ancora più semplice, se si arresta. È qualcosa che il compilatore non può fare.


6

Davvero sorpreso che non ci sia una risposta che usi direttamente il problema dell'arresto! C'è una riduzione molto diretta da questo problema al problema dell'arresto.

Immagina che il compilatore possa dire se una funzione ha cambiato o meno il valore di una variabile. Quindi sarebbe sicuramente in grado di dire se la seguente funzione cambia il valore di y oppure no, assumendo che il valore di x possa essere tracciato in tutte le chiamate durante il resto del programma:

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

Ora, per qualsiasi programma che ci piace, riscriviamolo come:

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

Si noti che, se, e solo se, il nostro programma cambia il valore di y, allora termina - foo () è l'ultima cosa che fa prima di uscire. Ciò significa che abbiamo risolto il problema dell'arresto!

Ciò che la riduzione di cui sopra ci mostra è che il problema di determinare se il valore di una variabile cambia è difficile almeno quanto il problema dell'arresto. Il problema dell'arresto è noto per essere incomputabile, quindi deve esserlo anche questo.


Non sono sicuro di seguire il tuo ragionamento sul motivo per cui il nostro programma termina se e solo se cambia il valore di y. A me sembra che foo()ritorni velocemente e poi main()esca. (Inoltre, chiami foo()senza discutere ... fa parte della mia confusione.)
LarsH

1
@LarsH: Se il programma modificato termina, l'ultima funzione che ha chiamato era f. Se y è stato modificato, è stata chiamata f (le altre istruzioni non possono cambiare y, poiché è stata introdotta solo dalla modifica). Quindi, se y è stato modificato, il programma termina.
MSalters

4

Non appena una funzione chiama un'altra funzione di cui il compilatore non "vede" l'origine, deve presumere che la variabile sia cambiata, altrimenti le cose potrebbero andare storte più avanti. Ad esempio, supponiamo di avere questo in "foo.cpp":

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

e abbiamo questo in "bar.cpp":

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

Come può il compilatore "sapere" che xnon sta cambiando (o sta cambiando, in modo più appropriato) bar?

Sono sicuro che possiamo inventare qualcosa di più complesso, se questo non è abbastanza complesso.


Il compilatore può sapere che x non cambia in bar se bar x viene passato come passaggio per riferimento a const, giusto?
Giocatore di cricket dal

Sì, ma se aggiungo un const_castin pippo, cambierebbe comunque x- violerei il contratto che dice che non devi modificare le constvariabili, ma dal momento che puoi convertire qualsiasi cosa in "più const" ed const_castesiste, i progettisti del linguaggio avevano sicuramente in mente che a volte ci sono buone ragioni per credere che i constvalori potrebbero dover cambiare.
Mats Petersson

@MatsPetersson: credo che se const_cast riesci a mantenere tutti i pezzi che si rompono perché il compilatore può, ma non deve compensarlo.
Zan Lynx

@ZanLynx: Sì, sono sicuro che sia corretto. Ma allo stesso tempo, il cast esiste, il che significa che qualcuno che ha progettato il linguaggio aveva una sorta di idea che "potremmo aver bisogno di questo ad un certo punto" - il che significa che non è destinato a non fare nulla di utile.
Mats Petersson

1

In generale è impossibile per il compilatore determinare se la variabile lo farà modificata, come è stato sottolineato.

Quando si controlla const-ness, la domanda di interesse sembra essere se la variabile può essere modificata da una funzione. Anche questo è difficile nelle lingue che supportano i puntatori. Non puoi controllare cosa fa l'altro codice con un puntatore, potrebbe anche essere letto da una fonte esterna (anche se improbabile). Nei linguaggi che limitano l'accesso alla memoria, questi tipi di garanzie possono essere possibili e consentono un'ottimizzazione più aggressiva rispetto al C ++.


2
Una cosa che vorrei fosse supportata nelle lingue sarebbe una distinzione tra riferimenti (o puntatori) effimeri, restituibili e persistenti. I riferimenti effimeri possono essere copiati solo in altri riferimenti effimeri, quelli restituibili possono essere copiati in quelli effimeri o restituibili e quelli persistenti possono essere copiati in qualsiasi modo. Il valore di ritorno di una funzione sarà vincolato dal più restrittivo degli argomenti passati come parametri "restituibili". Considero un peccato che in molte lingue, quando si passa un riferimento, non ci sia nulla che indichi per quanto tempo può essere utilizzato.
supercat

Sarebbe sicuramente utile. Ci sono ovviamente modelli per questo, ma in C ++ (e molti altri linguaggi) è sempre possibile "barare".
Krumelur

Un modo importante in cui .NET è superiore a Java è che ha il concetto di un riferimento effimero, ma sfortunatamente non c'è modo per gli oggetti di esporre le proprietà come riferimenti effimeri (quello che mi piacerebbe vedere sarebbe un mezzo per quale codice utilizzando una proprietà passerebbe un riferimento effimero a un codice (insieme a variabili temporanee) che dovrebbe essere usato per manipolare l'oggetto.
supercat

1

Per rendere la domanda più specifica, suggerisco che il seguente insieme di vincoli potrebbe essere stato ciò che l'autore del libro potrebbe aver avuto in mente:

  1. Supponiamo che il compilatore stia esaminando il comportamento di una funzione specifica rispetto alla costanza di una variabile. Per correttezza, un compilatore dovrebbe assumere (a causa dell'aliasing come spiegato di seguito) se la funzione chiama un'altra funzione la variabile viene modificata, quindi l'ipotesi n. 1 si applica solo ai frammenti di codice che non effettuano chiamate di funzione.
  2. Supponiamo che la variabile non venga modificata da un'attività asincrona o simultanea.
  3. Supponiamo che il compilatore determini solo se la variabile può essere modificata, non se verrà modificata. In altre parole, il compilatore esegue solo l'analisi statica.
  4. Supponiamo che il compilatore stia considerando solo il codice che funziona correttamente (senza considerare overrun / underrun di array, puntatori errati, ecc.)

Nel contesto della progettazione del compilatore, penso che i presupposti 1,3,4 abbiano perfettamente senso nella vista di uno scrittore di compilatori nel contesto della correttezza della generazione del codice e / o dell'ottimizzazione del codice. L'assunzione 2 ha senso in assenza della parola chiave volatile. E questi presupposti focalizzano anche la domanda abbastanza da rendere il giudizio di una risposta proposta molto più definitivo :-)

Dati questi presupposti, un motivo chiave per cui non si può presumere costanza è dovuto all'aliasing delle variabili. Il compilatore non può sapere se un'altra variabile punta alla variabile const. L'aliasing potrebbe essere dovuto a un'altra funzione nella stessa unità di compilazione, nel qual caso il compilatore potrebbe esaminare le funzioni e utilizzare un albero delle chiamate per determinare staticamente che potrebbe verificarsi l'aliasing. Ma se l'aliasing è dovuto a una libreria o altro codice esterno, il compilatore non ha modo di sapere al momento dell'ingresso nella funzione se le variabili sono alias.

Si potrebbe sostenere che se una variabile / argomento è contrassegnato come const, non dovrebbe essere soggetto a modifiche tramite aliasing, ma per uno scrittore di compilatore è piuttosto rischioso. Può anche essere rischioso per un programmatore umano dichiarare una variabile const come parte di, ad esempio, un grande progetto in cui non conosce il comportamento dell'intero sistema, o il sistema operativo, o una libreria, per conoscere davvero una variabile ha vinto ' t cambiare.


0

Anche se viene dichiarata una variabile const, non significa che un codice scritto male possa sovrascriverla.

//   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;
}

produzione:

1
2

Ciò accade perché ae bsono variabili di stack e si b[1]trova nella stessa posizione di memoria di a.
Mark Lakata

1
-1. Undefined Behavior rimuove tutte le restrizioni sul comportamento del compilatore.
MSalters

Non sono sicuro del voto negativo. Questo è solo un esempio che va alla domanda originale dell'OP sul perché un compilatore non può capire se qualcosa è veramente constse tutto è etichettato const. È perché il comportamento indefinito fa parte di C / C ++. Stavo cercando di trovare un modo diverso per rispondere alla sua domanda piuttosto che menzionare l'arresto del problema o l'input umano esterno.
Mark Lakata

0

Per espandere i miei commenti, il testo di quel libro non è chiaro, il che offusca il problema.

Come ho commentato, quel libro sta cercando di dire, "facciamo in modo che un numero infinito di scimmie scriva ogni funzione C ++ concepibile che potrebbe mai essere scritta. Ci saranno casi in cui se scegliamo una variabile che (una funzione particolare scritta dalle scimmie) utilizza, non possiamo capire se la funzione cambierà quella variabile. "

Ovviamente per alcune (anche molte) funzioni in una data applicazione, questo può essere determinato dal compilatore e molto facilmente. Ma non per tutti (o necessariamente per la maggior parte).

Questa funzione può essere facilmente analizzata:

static int global;

void foo()
{
}

"foo" chiaramente non modifica "globale". Non modifica assolutamente nulla e un compilatore può risolverlo molto facilmente.

Questa funzione non può essere così analizzata:

static int global;

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

Poiché le azioni di "pippo" dipendono da un valore che può cambiare in fase di esecuzione , chiaramente non puòessere determinato in fase di compilazione se modificherà "globale".

L'intero concetto è molto più semplice da capire di quanto gli scienziati informatici ritengano che sia. Se la funzione può fare qualcosa di diverso in base alle cose che possono cambiare in fase di esecuzione, allora non puoi capire cosa farà finché non viene eseguita e ogni volta che viene eseguita potrebbe fare qualcosa di diverso. Che sia provabilmente impossibile o meno, è ovviamente impossibile.


quello che dici è vero, ma anche per programmi molto semplici di cui tutto è noto in fase di compilazione non potrai provare nulla, nemmeno che il programma si fermerà. Questo è il problema che si ferma. Ad esempio potresti scrivere un programma basato su Hailstone Sequences en.wikipedia.org/wiki/Collatz_conjecture e farlo restituire true se converge a uno. I compilatori non saranno in grado di farlo (poiché in molti casi sarebbe traboccante) e persino i matematici non sanno se è vero o no.
kriss

Se intendi "ci sono alcuni programmi molto semplici per i quali non puoi provare nulla", sono completamente d'accordo. Ma la classica dimostrazione di Turing Halting Problem si basa essenzialmente sul fatto che un programma stesso sia in grado di dire se si ferma per creare una contraddizione. Poiché questa è matematica, non implementazione. Ci sono certamente programmi che è del tutto possibile determinare staticamente in fase di compilazione se una particolare variabile verrà modificata e se il programma si fermerà. Potrebbe non essere dimostrabile matematicamente, ma in alcuni casi è praticamente realizzabile.
El Zorko
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.