C è in realtà Turing completo?


40

Stavo cercando di spiegare a qualcuno che C è completo di Turing e mi sono reso conto che in realtà non so se tecnicamente sia completo di Turing. (C come nella semantica astratta, non come in un'implementazione effettiva.)

La risposta "ovvia" (approssimativamente: può indirizzare una quantità arbitraria di memoria, quindi può emulare una macchina RAM, quindi è Turing-complete) non è in realtà corretta, per quanto posso dire, come se lo standard C lo consenta affinché size_t sia arbitrariamente grande, deve essere fissato a una certa lunghezza e, indipendentemente dalla lunghezza a cui è fissato, è ancora finito. (In altre parole, anche se potresti, data una macchina Turing di arresto arbitraria, scegliere una lunghezza di size_t tale che funzionerà "correttamente", non c'è modo di scegliere una lunghezza di size_t tale che tutte le macchine di Turing che si fermano funzionino correttamente)

Quindi: C99 Turing è completo?


3
Quali sono la "semantica astratta" di C? Sono definiti ovunque?
Yuval Filmus,

4
@YuvalFilmus - Vedi ad esempio qui , ovvero C come definito nello standard anziché ad esempio "questo è il modo in cui gcc lo fa".
TLW,

1
c'è la "tecnicità" che i computer moderni non hanno memoria infinita come la TM, ma sono ancora considerati "computer universali". e nota che i linguaggi di programmazione nella loro "semantica" non assumono davvero una memoria finita, tranne per il fatto che tutte le loro implementazioni sono ovviamente limitate nella memoria. vedi ad es. il nostro PC funziona come una macchina di Turing . comunque essenzialmente tutti i linguaggi di programmazione "mainstream" sono Turing completi.
vzn,

2
C (come le macchine di Turing) non si limita all'uso della memoria interna del computer per i suoi calcoli, quindi questo non è un argomento valido contro la completezza di Turing di C.
reinierpost,

@reinierpost - è come dire che un teletipo è sapiente. Ciò significa che "C + un equivalente TM esterno" è Turing completo, non che C è Turing completo.
TLW,

Risposte:


34

Non sono sicuro ma penso che la risposta sia no, per ragioni piuttosto sottili. Ho chiesto a Teorica Informatica alcuni anni fa e non ho avuto una risposta che vada oltre ciò che presenterò qui.

Nella maggior parte dei linguaggi di programmazione, è possibile simulare una macchina Turing:

  • simulare l'automa finito con un programma che utilizza una quantità finita di memoria;
  • simulando il nastro con una coppia di liste collegate di numeri interi, che rappresentano il contenuto del nastro prima e dopo la posizione corrente. Spostare il puntatore significa trasferire la testa di una delle liste sull'altra lista.

Un'implementazione concreta in esecuzione su un computer esaurirebbe la memoria se il nastro fosse troppo lungo, ma un'implementazione ideale potrebbe eseguire fedelmente il programma della macchina di Turing. Questo può essere fatto con carta e penna, o acquistando un computer con più memoria e un compilatore indirizzato a un'architettura con più bit per parola e così via se il programma esaurisce la memoria.

Questo non funziona in C perché è impossibile avere un elenco collegato che può crescere per sempre: c'è sempre un limite al numero di nodi.

Per spiegare perché, devo prima spiegare cos'è un'implementazione in C. C è in realtà una famiglia di linguaggi di programmazione. Lo standard ISO C (più precisamente, una versione specifica di questo standard) definisce (con il livello di formalità che l'inglese consente) la sintassi e la semantica una famiglia di linguaggi di programmazione. C ha molti comportamenti indefiniti e comportamenti definiti dall'implementazione. Una "implementazione" di C codifica tutto il comportamento definito dall'implementazione (l'elenco delle cose da codificare è nell'appendice J per C99). Ogni implementazione di C è un linguaggio di programmazione separato. Si noti che il significato della parola "implementazione" è un po 'peculiare: ciò che significa in realtà è una variante di lingua, possono esserci più programmi di compilazione diversi che implementano la stessa variante di lingua.

In una data implementazione di C, un byte ha valori possibili. Tutti i dati possono essere rappresentati come una matrice di byte: un tipo ha al massimo possibili valori. Questo numero varia in diverse implementazioni di C, ma per una data implementazione di C, è una costante.2CHAR_BITt2CHAR_BIT×sizeof(t)

In particolare, i puntatori possono assumere al massimo . Ciò significa che esiste un numero massimo finito di oggetti indirizzabili.2CHAR_BIT×sizeof(void*)

I valori di CHAR_BITe sizeof(void*)sono osservabili, quindi se si esaurisce la memoria, non è possibile riprendere a eseguire il programma con valori maggiori per tali parametri. Eseguiresti il ​​programma con un linguaggio di programmazione diverso - un'implementazione C diversa.

Se i programmi in una lingua possono avere solo un numero limitato di stati, il linguaggio di programmazione non è più espressivo degli automi finiti. Il frammento di C che è limitato alla memoria indirizzabile consente al massimo indica dove è la dimensione dell'albero di sintassi astratto della programma (che rappresenta lo stato del flusso di controllo), quindi questo programma può essere simulato da un automa finito con molti stati. Se C è più espressivo, deve essere attraverso l'uso di altre funzionalità. nn×2CHAR_BIT×sizeof(void*)n

C non impone direttamente una profondità massima di ricorsione. Un'implementazione può avere un massimo, ma può anche non averne uno. Ma come possiamo comunicare tra una chiamata di funzione e il suo genitore? Gli argomenti non sono utili se sono indirizzabili, perché ciò limiterebbe indirettamente la profondità della ricorsione: se si dispone di una funzione, int f(int x) { … f(…) …}tutte le occorrenze xsu frame attivi di fhanno il proprio indirizzo e quindi il numero di chiamate nidificate è limitato dal numero di possibili indirizzi per x.

Il programma CA può utilizzare la memorizzazione non indirizzabile sotto forma di registervariabili. Le implementazioni "normali" possono avere solo un numero limitato e limitato di variabili che non hanno un indirizzo, ma in teoria un'implementazione potrebbe consentire una quantità illimitata di registerspazio di archiviazione. In una tale implementazione, è possibile effettuare una quantità illimitata di chiamate ricorsive a una funzione, purché lo siano i suoi argomenti register. Ma dal momento che gli argomenti sono register, non è possibile indicarli e quindi è necessario copiare i loro dati in modo esplicito: è possibile passare solo una quantità finita di dati, non una struttura di dati di dimensioni arbitrarie fatta di puntatori.

Con la profondità di ricorsione illimitata e la restrizione che una funzione può ottenere solo i dati dal suo chiamante diretto ( registerargomenti) e restituire i dati al suo chiamante diretto (il valore di ritorno della funzione), si ottiene la potenza di automi pushdown deterministici .

Non riesco a trovare un modo per andare oltre.

(Naturalmente è possibile fare in modo che il programma memorizzi il contenuto del nastro esternamente, tramite le funzioni di input / output del file. Ma non si chiederà se C è Turing completo, ma se C più un sistema di archiviazione infinito è Turing completo, per che la risposta è un noioso "sì". Si potrebbe anche definire l'archiviazione come un oracolo di Turing - chiamata  fopen("oracle", "r+"), fwriteil contenuto del nastro iniziale e freadindietro il contenuto del nastro finale.)


Non è immediatamente chiaro perché l'insieme di indirizzi debba essere finito. Ho scritto alcuni pensieri in una risposta alla domanda che colleghi: cstheory.stackexchange.com/a/37036/43393
Alexey B.

4
Siamo spiacenti, ma con la stessa logica, non esistono affatto linguaggi di programmazione completi di Turing. Ogni lingua ha una limitazione esplicita o implicita nello spazio degli indirizzi. Se si crea una macchina con memoria infinita, anche i puntatori ad accesso casuale avranno ovviamente una lunghezza infinita. Pertanto, se viene visualizzata una macchina del genere, dovrà offrire un set di istruzioni per l'accesso sequenziale alla memoria, insieme a un'API per linguaggi di alto livello.
Imil

14
@IMil Questo non è vero. Alcuni linguaggi di programmazione non hanno limiti allo spazio degli indirizzi, neppure implicitamente. Per fare un esempio ovvio, una macchina Turing universale in cui lo stato iniziale del nastro costituisce il programma è un linguaggio di programmazione completo di Turing. Molti linguaggi di programmazione effettivamente utilizzati nella pratica hanno la stessa proprietà, ad esempio Lisp e SML. Una lingua non deve avere un concetto di "puntatore ad accesso casuale". (cont.)
Gilles 'SO- smetti di essere malvagio' il

11
@IMil (cont.) Le implementazioni di solito fanno per le prestazioni, ma sappiamo che un'implementazione in esecuzione su un determinato computer non è Turing completa poiché è limitata dalle dimensioni della memoria del computer. Ciò significa che l'implementazione non implementa l'intero linguaggio, ma solo un sottoinsieme (di programmi in esecuzione in N byte di memoria). È possibile eseguire il programma su un computer e, se esaurisce la memoria, spostarlo su un computer più grande e così via per sempre o fino a quando non si arresta. Sarebbe un modo valido per implementare l'intera lingua.
Gilles 'SO- smetti di essere malvagio' il

6

L'aggiunta di va_copyC99 all'argomento variadico API può darci una via di accesso alla completezza di Turing. Dal momento che diventa possibile scorrere più volte una lista di argomenti variadici in una funzione diversa da quella che ha originariamente ricevuto gli argomenti, va_argspuò essere utilizzato per implementare un puntatore senza puntatore.

Ovviamente, un'implementazione reale dell'argomento dell'API variadica probabilmente avrà un puntatore da qualche parte, ma nella nostra macchina astratta può invece essere implementata usando la magia.

Ecco una demo che implementa un automa pushdown a 2 stack con regole di transizione arbitrarie:

#include <stdarg.h>
typedef struct { va_list va; } wrapped_stack; // Struct wrapper needed if va_list is an array type.
#define NUM_SYMBOLS /* ... */
#define NUM_STATES /* ... */
typedef enum { NOP, POP1, POP2, PUSH1, PUSH2 } operation_type;
typedef struct { int next_state; operation_type optype; int opsymbol; } transition;
transition transition_table[NUM_STATES][NUM_SYMBOLS][NUM_SYMBOLS] = { /* ... */ };

void step(int state, va_list stack1, va_list stack2);
void push1(va_list stack2, int next_state, ...) {
    va_list stack1;
    va_start(stack1, next_state);
    step(next_state, stack1, stack2);
}
void push2(va_list stack1, int next_state, ...) {
    va_list stack2;
    va_start(stack2, next_state);
    step(next_state, stack1, stack2);
}
void step(int state, va_list stack1, va_list stack2) {
    va_list stack1_copy, stack2_copy;
    va_copy(stack1_copy, stack1); va_copy(stack2_copy, stack2);
    int symbol1 = va_arg(stack1_copy, int), symbol2 = va_arg(stack2_copy, int);
    transition tr = transition_table[state][symbol1][symbol2];
    wrapped_stack ws;
    switch(tr.optype) {
        case NOP: step(tr.next_state, stack1, stack2);
        // Note: attempting to pop the stack's bottom value results in undefined behavior.
        case POP1: ws = va_arg(stack1_copy, wrapped_stack); step(tr.next_state, ws.va, stack2);
        case POP2: ws = va_arg(stack2_copy, wrapped_stack); step(tr.next_state, stack1, ws.va);
        case PUSH1: va_copy(ws.va, stack1); push1(stack2, tr.next_state, tr.opsymbol, ws);
        case PUSH2: va_copy(ws.va, stack2); push2(stack1, tr.next_state, tr.opsymbol, ws);
    }
}
void start_helper1(va_list stack1, int dummy, ...) {
    va_list stack2;
    va_start(stack2, dummy);
    step(0, stack1, stack2);
}
void start_helper0(int dummy, ...) {
    va_list stack1;
    va_start(stack1, dummy);
    start_helper1(stack1, 0, 0);
}
// Begin execution in state 0 with each stack initialized to {0}
void start() {
    start_helper0(0, 0);
}

Nota: se va_listè un tipo di array, in realtà ci sono parametri puntatore nascosti nelle funzioni. Quindi sarebbe probabilmente meglio cambiare i tipi di tutti gli va_listargomenti in wrapped_stack.


Questo potrebbe funzionare. Una possibile preoccupazione è che si basi sull'allocazione di un numero illimitato di va_listvariabili automatiche stack. Queste variabili devono avere un indirizzo &stacke possiamo avere solo un numero limitato di queste. Questo requisito potrebbe essere aggirato dichiarando ogni variabile locale register, forse?
chi,

@chi AIUI non è necessario che una variabile abbia un indirizzo a meno che qualcuno non tenti di prendere l'indirizzo. Inoltre, è illegale dichiarare l'argomento che precede immediatamente i puntini di sospensione register.
feersum

Secondo la stessa logica, non dovrebbe intessere richiesto di avere un limite a meno che qualcuno non usi il limite o sizeof(int)?
chi,

@chi Niente affatto. Lo standard definisce come parte della semantica astratta che un intha un valore tra alcuni limiti finiti INT_MINe INT_MAX. E se il valore di un intoverflow supera questi limiti, si verifica un comportamento indefinito. D'altra parte, lo standard non richiede intenzionalmente che tutti gli oggetti siano fisicamente presenti nella memoria in un determinato indirizzo, in quanto ciò consente ottimizzazioni come la memorizzazione dell'oggetto in un registro, l'archiviazione solo di una parte dell'oggetto, rappresentandolo in modo diverso rispetto allo standard layout o omettendolo del tutto se non è necessario.
feersum

4

Aritmetica non standard, forse?

Quindi, sembra che il problema sia la dimensione finita di sizeof(t). Tuttavia, penso di conoscere un lavoro in giro.

Per quanto ne so, C non richiede un'implementazione per usare gli interi standard per il suo tipo intero. Pertanto, potremmo usare un modello aritmetico non standard . Quindi, impostiamo sizeof(t)un numero non standard e ora non lo raggiungeremo mai in un numero finito di passaggi. Pertanto, la lunghezza del nastro delle macchine di Turing sarà sempre inferiore al "massimo", poiché il massimo è letteralmente impossibile da raggiungere. sizeof(t)semplicemente non è un numero nel senso normale della parola.

Questo è un tecnicismo ovviamente: il teorema di Tennenbaum . Afferma che l'unico modello dell'aritmetica di Peano è quello standard, cosa che ovviamente non farebbe. Tuttavia, per quanto ne so, C non richiede implementazioni per utilizzare tipi di dati che soddisfino gli assiomi di Peano, né richiede che l'implementazione sia calcolabile, quindi questo non dovrebbe essere un problema.

Cosa dovrebbe accadere se si tenta di generare un numero intero non standard? Bene, puoi rappresentare qualsiasi numero intero non standard utilizzando una stringa non standard, quindi esegui semplicemente lo streaming di cifre dalla parte anteriore di quella stringa.


Come sarebbe un'implementazione?
reinierpost,

@reinierpost Immagino che rappresenterebbe i dati usando un modello numerabile non standard di PA. Calcolerebbe le operazioni aritmetiche usando un grado PA . Penso che qualsiasi modello del genere dovrebbe fornire un'implementazione C valida.
PyRulez,

Siamo spiacenti, questo non funziona. sizeof(t)è esso stesso un valore di tipo size_t, quindi è un numero intero naturale compreso tra 0 e SIZE_MAX.
Gilles 'SO- smetti di essere malvagio' il

@Gilles SIZE_MAX sarebbe anche un naturale non standard.
PyRulez,

Questo è un approccio interessante. Nota che avresti anche bisogno di intptr_t / uintptr_t / ptrdiff_t / intmax_t / uintmax_t per essere non standard. In C ++ ciò costituirebbe una violazione delle garanzie di avanzamento ... non sono sicuro di C.
TLW il

0

IMO, una forte limitazione è che lo spazio indirizzabile (tramite la dimensione del puntatore) è finito, e questo è irrecuperabile.

Si potrebbe sostenere che la memoria può essere "scambiata su disco", ma a un certo punto le informazioni sull'indirizzo supereranno di per sé le dimensioni indirizzabili.


Non è questo il punto principale della risposta accettata? Non credo che aggiunga nulla di nuovo alle risposte a questa domanda del 2016.
chi

@chi: no, la risposta accettata non menziona lo scambio con la memoria esterna, che potrebbe essere considerata una soluzione alternativa.
Yves Daoust

-1

In pratica, queste restrizioni sono irrilevanti per la completezza di Turing. Il vero requisito è consentire al nastro di essere arbitrariamente lungo, non infinito. Ciò creerebbe un problema di arresto di diverso tipo (in che modo l'universo "calcola" il nastro?)

È fasullo come dire "Python non è Turing completo perché non puoi fare una lista infinitamente grande".

[Modifica: grazie a Mr. Whitledge per aver chiarito come modificare.]


7
Non credo che questo risponda al quesiton. La domanda ha già anticipato questa risposta e ha spiegato perché non è valida: "sebbene lo standard C consenta che size_t sia arbitrariamente grande, deve essere fissato a una certa lunghezza e, indipendentemente dalla lunghezza a cui è fissato, è ancora finito ". Hai qualche risposta a tale argomento? Non credo che possiamo considerare la domanda come risposta a meno che la risposta non spieghi perché tale argomento sia sbagliato (o giusto).
DW

5
In qualsiasi momento, un valore di tipo size_tè finito. Il problema è che non è possibile stabilire un limite size_tvalido per tutto il calcolo: per qualsiasi limite, un programma potrebbe traboccare. Ma il linguaggio C afferma che esiste un limite per size_t: su una data implementazione, può solo crescere fino a sizeof(size_t)byte. Inoltre, sii gentile . Dire che le persone che ti criticano "non possono pensare da sole" è scortese.
Gilles 'SO- smetti di essere malvagio' il

1
Questa è la risposta corretta Un tornio non richiede un nastro infinito, richiede un nastro "arbitrariamente lungo". Cioè, puoi supporre che il nastro sia lungo quanto deve essere per completare il calcolo. Puoi anche supporre che il tuo computer abbia tutta la memoria di cui ha bisogno. Un nastro infinito non è assolutamente necessario, poiché qualsiasi calcolo che si interrompe a tempo finito non può utilizzare una quantità infinita di nastro.
Jeffrey L Whitledge,

Ciò che questa risposta mostra è che per ogni TM è possibile scrivere un'implementazione in C con una lunghezza del puntatore sufficiente per simularla. E ', tuttavia, non è possibile scrivere un'implementazione C che può simulare qualsiasi TM. Quindi la specifica proibisce che ogni particolare implementazione sia T-complete. Non è nemmeno T-complete, poiché la lunghezza del puntatore è fissa.

1
Questa è un'altra risposta corretta che è appena visibile a causa dell'incapacità della maggior parte degli individui in questa comunità. Nel frattempo, la risposta accettata è falsa e la sua sezione dei commenti è sorvegliata dai moderatori che eliminano i commenti critici. Ciao, c.stackexchange.
Xamid,

-1

I supporti rimovibili ci consentono di aggirare il problema di memoria illimitata. Forse la gente penserà che questo sia un abuso, ma penso che sia OK e comunque inevitabile.

Risolve qualsiasi implementazione di una macchina Turing universale. Per il nastro, utilizziamo supporti rimovibili. Quando la testina termina alla fine o all'inizio del disco corrente, la macchina richiede all'utente di inserire il successivo o il precedente. Possiamo usare un marcatore speciale per indicare l'estremità sinistra del nastro simulato o avere un nastro non legato in entrambe le direzioni.

Il punto chiave qui è che tutto ciò che il programma C deve fare è finito. Il computer ha solo bisogno di memoria sufficiente per simulare l'automa e size_tdeve essere abbastanza grande da consentire di indirizzare quella (in realtà piuttosto piccola) quantità di memoria e sui dischi, che possono avere qualsiasi dimensione finita fissa. Poiché all'utente viene richiesto solo di inserire il disco successivo o precedente, non abbiamo bisogno di numeri interi grandi e illimitati per dire "Inserire il numero del disco 123456 ..."

Suppongo che l'obiezione principale sia probabilmente quella del coinvolgimento dell'utente, ma ciò sembra inevitabile in qualsiasi implementazione, perché non sembra esserci altro modo di implementare la memoria illimitata.


3
Direi che, a meno che la definizione C non richieda tale archiviazione esterna illimitata, ciò non può essere accettato come prova della completezza di Turing. (ISO 9899 non richiede che, naturalmente, sia scritto per l'ingegneria del mondo reale.) Ciò che mi preoccupa è che, se lo accettiamo, con un ragionamento simile potremmo affermare che i DFA sono Turing completi, poiché potrebbero essere utilizzati per guidare una testa su un nastro (la memoria esterna).
chi,

@chi Non vedo come segue l'argomento DFA. Il punto centrale di un DFA è che ha solo accesso in lettura all'archiviazione. Se gli permetti di "guidare una testina su un nastro", non è esattamente una macchina di Turing?
David Richerby,

2
Anzi, sto puntando un po 'qui. Il punto è: perché è OK aggiungere un "nastro" a C, lasciare che C simuli un DFA e usare questo fatto per affermare che C è Turing completo, quando non possiamo fare lo stesso con DFA? Se C non ha modo di implementare la memoria illimitata da solo, non dovrebbe essere considerato Turing completo. (Lo definirei ancora "moralmente" Turing completo, almeno, poiché i limiti sono così grandi che nella pratica non contano nella maggior parte dei casi) Penso che per risolvere definitivamente la questione, occorrerebbe una rigorosa specifica formale di C (lo standard ISO non è sufficiente)
chi

1
@chi Va bene perché C include routine di I / O dei file. I DFA no.
David Richerby,

1
C non specifica completamente cosa fanno queste routine - la maggior parte della loro semantica è definita dall'implementazione. L'implementazione AC non è necessaria per memorizzare il contenuto del file, ad esempio, penso che possa comportarsi come se ogni file fosse "/ dev / null", per così dire. Non è inoltre necessario archiviare una quantità illimitata di dati. Direi che il tuo argomento è corretto, considerando ciò che fa la stragrande maggioranza delle implementazioni in C e generalizzando quel comportamento a una macchina ideale. Se facciamo solo affidamento sulla definizione C, dimenticando la pratica, non credo che valga.
chi,

-2

Scegli size_tdi essere infinitamente grande

Puoi scegliere size_tdi essere infinitamente grande. Naturalmente, è impossibile realizzare una tale implementazione. Ma non è una sorpresa, data la natura limitata del mondo in cui viviamo.

Implicazioni pratiche

Ma anche se fosse possibile realizzare tale implementazione, ci sarebbero problemi pratici. Considera la seguente dichiarazione C:

printf("%zu\n",SIZE_MAX);

SIZE_MAXSIZE_MAXO(2Sioze_t)size_tSIZE_MAXprintf

Fortunatamente, per i nostri scopi teorici, non ho trovato alcun requisito nella specifica che le garanzie printfterminino per tutti gli input. Quindi, per quanto ne so, non violiamo le specifiche C qui.

Sulla completezza computazionale

Resta ancora da dimostrare che la nostra implementazione teorica è Turing Complete . Possiamo dimostrarlo implementando "qualsiasi Turing Machine a nastro singolo".

Molti di noi hanno probabilmente implementato una Turing Machine come progetto scolastico. Non fornirò i dettagli di un'implementazione specifica, ma ecco una strategia comunemente usata:

  • Il numero di stati, il numero di simboli e la tabella di transizione degli stati sono fissi per ogni macchina. Quindi possiamo rappresentare stati e simboli come numeri e la tabella di transizione di stato come un array bidimensionale.
  • Il nastro può essere rappresentato come un elenco collegato. Possiamo utilizzare un singolo elenco a doppio collegamento o due elenchi a collegamento singolo (uno per ogni direzione dalla posizione corrente).

Ora vediamo cosa è necessario per realizzare tale implementazione:

  • La capacità di rappresentare alcuni insiemi di numeri fissi, ma arbitrariamente grandi. Per rappresentare qualsiasi numero arbitrario, scegliamo MAX_INTdi essere anche infiniti. (In alternativa, potremmo usare altri oggetti per rappresentare stati e simboli.)
  • La capacità di costruire un elenco di collegamenti arbitrariamente grande per il nostro nastro. Ancora una volta, non esiste un limite finito per le dimensioni. Ciò significa che non possiamo costruire questo elenco in anticipo, poiché spenderemmo per sempre solo per costruire il nostro nastro. Ma possiamo costruire questo elenco in modo incrementale se utilizziamo l'allocazione dinamica della memoria. Possiamo usare malloc, ma c'è un po 'di più che dobbiamo considerare:
    • La specifica C consente mallocdi fallire se, ad esempio, la memoria disponibile è stata esaurita. Quindi la nostra implementazione è veramente universale se mallocnon fallisce mai.
    • Tuttavia, se la nostra implementazione viene eseguita su una macchina con memoria infinita, non è necessario mallocfallire. Senza violare lo standard C, la nostra implementazione garantirà che mallocnon fallirà mai.
  • La capacità di dereferenziare i puntatori, cercare gli elementi dell'array e accedere ai membri di un nodo elenco collegato.

Quindi l'elenco sopra è ciò che è necessario per implementare una Turing Machine nella nostra ipotetica implementazione C. Queste funzionalità devono terminare. Tuttavia, qualsiasi altra cosa può essere autorizzata a non terminare (a meno che non sia richiesto dalla norma). Ciò include l'aritmetica, l'IO, ecc.


6
Cosa printf("%zu\n",SIZE_MAX);stamperebbe su tale implementazione?
Ruslan,

1
@Ruslan, tale implementazione è impossibile, proprio come è impossibile implementare una macchina Turing. Ma se una tale implementazione fosse possibile, immagino che stamperebbe una rappresentazione decimale di un numero infinitamente grande - presumibilmente, un flusso infinito di cifre decimali.
Nathan Davis,

2
@NathanDavis È possibile implementare una macchina Turing. Il trucco è che non è necessario creare un nastro infinito: è sufficiente creare la porzione usata del nastro in modo incrementale, se necessario.
Gilles 'SO- smetti di essere malvagio' il

2
@Gilles: In questo universo finito in cui viviamo, è impossibile implementare una macchina di Turing.
gnasher729,

1
@NathanDavis Ma se lo fai, allora hai cambiato sizeof(size_t)(o CHAR_BITS). Non puoi riprendere dal nuovo stato, devi ricominciare, ma l'esecuzione del programma potrebbe essere diversa ora che quelle costanti sono diverse
SO di Gilles

-2

L'argomento principale qui era che la dimensione di size_t è finita, sebbene possa essere infinitamente grande.

C'è una soluzione alternativa, anche se non sono sicuro che questo coincida con ISO C.

Supponiamo di avere una macchina con memoria infinita. Quindi non sei limitato per la dimensione del puntatore. Hai ancora il tuo tipo size_t. Se mi chiedi cos'è sizeof (size_t) la risposta sarà solo sizeof (size_t). Se chiedi se è maggiore di 100, ad esempio, la risposta è sì. Se chiedi cos'è sizeof (size_t) / 2 come puoi immaginare, la risposta è ancora sizeof (size_t). Se vuoi stamparlo possiamo concordare un po 'di output. La differenza di questi due può essere NaN e così via.

Il riassunto è che il rilassamento della condizione per size_t ha dimensioni finite non romperà alcun programma già esistente.

PS Allocare la dimensione della memoria (size_t) è ancora possibile, hai solo bisogno di dimensioni numerabili, quindi diciamo che prendi tutti i pari (o un trucco simile).


1
"La differenza di questi due può essere NaN". No, non può essere. Non esiste una NaN di tipo intero in C.
TLW

Secondo lo standard, sizeofdeve restituire a size_t. Quindi devi scegliere un valore particolare.
Draconis il

-4

Sì.

1. Risposta tra virgolette

Come reazione all'elevata quantità di voti negativi sulle mie (e altre) risposte corrette - in confronto all'approvazione allarmante di false risposte - ho cercato una spiegazione alternativa meno teoricamente profonda. Ho trovato questo . Spero che copra alcuni degli errori comuni qui, in modo che venga mostrato un po 'più di intuizione. Parte essenziale dell'argomento:

[...] La sua argomentazione è la seguente: supponiamo che uno abbia scritto un determinato programma di terminazione che potrebbe richiedere, durante la sua esecuzione, fino a una quantità arbitraria di memoria. Senza cambiare quel programma, è possibile a posteriori implementare un componente hardware del computer e il suo compilatore C che forniscono spazio sufficiente per accogliere questo calcolo. Ciò potrebbe richiedere l'ampliamento della larghezza del carattere (tramite CHAR_BITS) e / o dei puntatori (tramite dimensione_t), ma non è necessario modificare il programma. Poiché ciò è possibile, C è davvero Turing-Complete per la chiusura dei programmi.

La parte difficile di questo argomento è che funziona solo quando si considera la conclusione dei programmi. I programmi di terminazione hanno questa bella proprietà che hanno un limite superiore statico al loro requisito di archiviazione, che si può determinare sperimentalmente eseguendo il programma sull'input desiderato con dimensioni di archiviazione crescenti, fino a quando non si adatta.

Il motivo per cui sono stato indotto in errore nel mio treno di pensieri è che stavo considerando la più ampia classe di programmi "utili" non terminanti [...]

In breve, poiché per ogni funzione calcolabile esiste una soluzione nel linguaggio C (a causa di limiti superiori illimitati), ogni problema calcolabile ha un programma C, quindi C è Turing completo.

2. La mia risposta originale

Esistono confusioni diffuse tra concetti matematici nell'informatica teorica (come la completezza di Turing) e la loro applicazione nel mondo reale, vale a dire tecniche nell'informatica pratica. La completezza di Turing non è una proprietà di macchine fisicamente esistenti o di qualsiasi modello limitato nello spazio-tempo. È solo un oggetto astratto che descrive le proprietà delle teorie matematiche.

C99 è Turing completo indipendentemente dalle restrizioni basate sull'implementazione, proprio come praticamente qualsiasi altro linguaggio di programmazione comune, poiché è in grado di esprimere un set funzionalmente completo di connettivi logici e in linea di principio ha accesso a una quantità illimitata di memoria. La gente ha sottolineato che C limita esplicitamente la limitazione dell'accesso alla memoria casuale, ma questo non è nulla che non si possa eludere, dal momento che si tratta di ulteriori restrizioni dichiarate nello standard C, mentre la completezza di Turing è già senza di esse :

Ecco una cosa basilare sui sistemi logici che dovrebbe essere sufficiente per una dimostrazione non costruttiva . Considera un calcolo con alcuni schemi e regole di assioma, in modo tale che l'insieme di conseqenze logiche sia X. Ora se aggiungi alcune regole o assiomi, l'insieme di conseguenze logiche cresce, cioè deve essere un superset di X. Ecco perché, ad esempio , la logica modale S4 è correttamente contenuta in S5. Allo stesso modo, quando si ha una sottospecifica che è Turing completa, ma si aggiungono alcune restrizioni in alto, queste non impediscono nessuna delle conseguenze in X, cioè ci deve essere un modo per aggirare tutte le restrizioni. Se si desidera una lingua non completa di Turing, il calcolo deve essere ridotto, non esteso. Le estensioni che sostengono che qualcosa non sarebbe possibile, ma in realtà lo sono, aggiungono solo incoerenze. Queste incoerenze nello standard C non possono tuttavia avere conseguenze pratiche, proprio come la completezza di Turing non è correlata all'applicazione pratica.

La simulazione di numeri arbitrari in base alla profondità di ricorsione (vale a dire questo ; con la possibilità di supportare più numeri tramite schedulazione / pseudo-thread; non esiste un limite teorico alla profondità di ricorsione in C ) o l'utilizzo dell'archiviazione dei file per simulare memoria di programma illimitata ( idea ) sono probabilmente solo due delle infinite possibilità per provare costruttivamente la completezza di Turing di C99. Bisogna ricordare che per la calcolabilità, la complessità del tempo e dello spazio sono irrilevanti. In particolare, assumere un ambiente limitato per falsificare la completezza di Turing è semplicemente un ragionamento circolare poiché tale limitazione esclude tutti i problemi che superano il presupposto limite di complessità.

( NOTA : ho scritto solo questa risposta per impedire alle persone di essere arrestate per ottenere intuizione matematica a causa di un tipo di pensiero limitato orientato all'applicazione. È un peccato che la maggior parte degli studenti leggerà la risposta falsa accettata a causa del suo voto basato su difetti fondamentali del ragionamento, in modo che più persone diffondano tali false credenze. Se declassate questa risposta, siete solo una parte del problema.)


4
Non seguo il tuo ultimo paragrafo. Sostieni che l'aggiunta di restrizioni aumenta il potere espressivo, ma chiaramente non è vero. Le restrizioni possono solo diminuire la potenza espressiva. Ad esempio, se prendi C e aggiungi la restrizione che nessun programma può accedere a più di 640kb di memoria (di qualsiasi tipo), allora l'hai trasformato in un elegante automa finito che chiaramente non è completo di Turing.
David Richerby,

3
Se disponi di una quantità fissa di spazio di archiviazione, non puoi simulare nulla che richieda più risorse di quante ne hai. Ci sono solo molte configurazioni in cui la tua memoria può essere, il che significa che ci sono solo molte cose che puoi fare.
David Richerby,

2
Non capisco perché ti riferisci a "macchine fisicamente esistenti". Si noti che la completezza di Turing è una proprietà di un modello computazionale matematico, non di sistemi fisici. Concordo sul fatto che nessun sistema fisico, essendo un oggetto finito, può avvicinarsi al potere delle macchine di Turing, ma questo è irrilevante. Possiamo ancora prendere qualsiasi linguaggio di programmazione, considerare la definizione matematica della sua semantica e verificare se quell'oggetto matematico è completo di Turing. Il gioco della vita di Conway è Turing potente anche se non c'è alcuna possibile implementazione fisica.
Chi

2
@xamid In caso di dubbi sulle politiche di moderazione di questo sito, portarlo su Computer Science Meta . Fino ad allora, per favore sii gentile . L'abuso verbale di altri non sarà tollerato. (Ho rimosso tutti i commenti che non riguardano l'argomento in questione.)
Raffaello

2
Dici che la modifica della larghezza di un puntatore non cambierebbe il programma, ma i programmi possono leggere la larghezza di un puntatore e fare quello che vogliono con quel valore. Lo stesso per CHAR_BITS.
Draconis l'
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.