C é realmente Turing completo?

40

Eu estava tentando explicar a alguém que C é Turing completo e percebi que na verdade não sei se é tecnicamente Turing completo. (C como na semântica abstrata, não como em uma implementação real.)

A resposta "óbvia" (grosso modo: ela pode endereçar uma quantidade arbitrária de memória, para emular uma máquina de RAM e, portanto, é completa em Turing) não é realmente correta, tanto quanto posso dizer, como se o padrão C permitisse para size_t ser arbitrariamente grande, ele deve ser fixado em algum comprimento e, independentemente do tamanho que for fixado, ele ainda é finito. (Em outras palavras, embora você possa, considerando uma máquina de Turing de parada arbitrária, escolher um tamanho_t de modo que ele funcione "corretamente", não há como escolher um tamanho_t de modo que todas as máquinas de Turing que parem funcionem corretamente)

Então: o C99 Turing está completo?

TLW
fonte
3
Quais são as "semânticas abstratas" de C? Eles são definidos em algum lugar?
Yuval Filmus
4
@YuvalFilmus - Veja, por exemplo , aqui , isto é, C, conforme definido no padrão, em oposição a, por exemplo, "é assim que o gcc faz".
TLW 26/07
11
existe a "tecnicidade" de que os computadores modernos não têm memória infinita como a MT, mas ainda são considerados "computadores universais". e observe que as linguagens de programação em sua "semântica" não assumem realmente uma memória finita, exceto que todas as suas implementações são obviamente limitadas na memória. veja, por exemplo , nosso PC funciona como uma máquina de Turing . de qualquer forma, essencialmente todas as linguagens de programação "mainstream" são Turing completas.
vzn
2
C (como máquinas de Turing) não se limita ao uso de memória do computador interno para seus cálculos, então isso realmente não é um argumento válido contra a completude de Turing de C.
reinierpost
@reinierpost - é como dizer que um teletipo é sapiente. Isso significa que "C + um equivalente externo à TM" é completo em Turing, não que C seja completo em Turing.
TLW

Respostas:

34

Não tenho certeza, mas acho que a resposta é não, por razões bastante sutis. Eu perguntei em Ciência da Computação Teórica há alguns anos e não obter uma resposta que vai além do que eu vou apresentar aqui.

Na maioria das linguagens de programação, você pode simular uma máquina de Turing:

  • simulando o autômato finito com um programa que usa uma quantidade finita de memória;
  • simulando a fita com um par de listas vinculadas de números inteiros, representando o conteúdo da fita antes e depois da posição atual. Mover o ponteiro significa transferir o cabeçalho de uma das listas para a outra lista.

Uma implementação concreta em execução em um computador ficaria sem memória se a fita ficasse muito longa, mas uma implementação ideal poderia executar o programa da máquina de Turing fielmente. Isso pode ser feito com caneta e papel ou comprando um computador com mais memória e um compilador visando uma arquitetura com mais bits por palavra e assim por diante, se o programa ficar sem memória.

Isso não funciona em C porque é impossível ter uma lista vinculada que pode crescer para sempre: sempre há algum limite no número de nós.

Para explicar o porquê, primeiro preciso explicar o que é uma implementação em C. C é realmente uma família de linguagens de programação. O padrão ISO C (mais precisamente, uma versão específica deste padrão) define (com o nível de formalidade que o inglês permite) a sintaxe e a semântica de uma família de linguagens de programação. C tem muitos comportamentos indefinidos e comportamentos definidos pela implementação. Uma "implementação" de C codifica todo o comportamento definido pela implementação (a lista de itens a serem codificados está no apêndice J da C99). Cada implementação de C é uma linguagem de programação separada. Observe que o significado da palavra “implementação” é um pouco peculiar: o que realmente significa é uma variante de linguagem, pode haver vários programas de compilação diferentes que implementam a mesma variante de linguagem.

Em uma determinada implementação de C, um byte possui valores possíveis de CHAR_BIT . Todos os dados podem ser representados como uma matriz de bytes: um tipo tem no máximo 2 CHAR_BIT × sizeof (t) valores possíveis. Esse número varia em diferentes implementações de C, mas para uma determinada implementação de C, é uma constante.2CHAR_BITt2CHAR_BIT×sizeof(t)

Em particular, os ponteiros podem ter no máximo . Isso significa que existe um número máximo finito de objetos endereçáveis.2CHAR_BIT×sizeof(void*)

Os valores de CHAR_BITe sizeof(void*)são observáveis; portanto, se você ficar sem memória, não poderá simplesmente continuar executando o programa com valores maiores para esses parâmetros. Você estaria executando o programa em uma linguagem de programação diferente - uma implementação em C diferente.

Se os programas em uma linguagem só podem ter um número limitado de estados, a linguagem de programação não é mais expressiva que os autômatos finitos. O fragmento de C restrito ao armazenamento endereçável permite apenas no máximo onde n é o tamanho da árvore de sintaxe abstrata do programa (representando o estado do fluxo de controle); programa pode ser simulado por um autômato finito com tantos estados. Se C é mais expressivo, deve ser através do uso de outros recursos.n×2CHAR_BIT×sizeof(void*)n

C não impõe diretamente uma profundidade máxima de recursão. É permitido que uma implementação tenha um máximo, mas também não é permitido. Mas como nos comunicamos entre uma chamada de função e seu pai? Os argumentos não são bons se forem endereçáveis, porque isso indiretamente limitaria a profundidade da recursão: se você tiver uma função int f(int x) { … f(…) …}, todas as ocorrências de xquadros ativos fterão seu próprio endereço e, portanto, o número de chamadas aninhadas será limitado pelo número de endereços possíveis para x.

O programa CA pode usar armazenamento não endereçável na forma de registervariáveis. As implementações “normais” podem ter apenas um número pequeno e finito de variáveis ​​que não têm um endereço, mas, em teoria, uma implementação pode permitir uma quantidade ilimitada de registerarmazenamento. Em tal implementação, você pode fazer uma quantidade ilimitada de chamadas recursivas para uma função, desde que seu argumento seja register. Mas, como os argumentos são register, você não pode fazer um ponteiro para eles e, portanto, precisa copiar explicitamente os dados deles: só é possível transmitir uma quantidade finita de dados, não uma estrutura de dados de tamanho arbitrário feita de ponteiros.

Com profundidade de recursão ilimitada e a restrição de que uma função pode obter apenas dados do chamador direto ( registerargumentos) e retornar dados ao chamador direto (o valor de retorno da função), você obtém o poder dos autômatos de empilhamento determinístico .

Não consigo encontrar um caminho a percorrer.

(É claro que você pode fazer com que o programa armazene o conteúdo da fita externamente, através das funções de entrada / saída de arquivo. Mas você não perguntaria se C é Turing-completo, mas se C mais um sistema de armazenamento infinito é Turing-completo, para qual a resposta é um chato "sim". Você também pode definir o armazenamento como um oráculo de Turing - chamada  fopen("oracle", "r+"), fwriteo conteúdo inicial da fita e freado conteúdo final da fita.)

Gilles 'SO- parar de ser mau'
fonte
Não está claro imediatamente por que o conjunto de endereços deve ser finito. Eu escrevi alguns pensamentos em uma resposta para a pergunta que você link: cstheory.stackexchange.com/a/37036/43393
Alexey B.
4
Desculpe, mas pela mesma lógica, não há linguagens de programação completas de Turing. Cada idioma tem uma limitação explícita ou implícita no espaço de endereço. Se você criar uma máquina com memória infinita, os ponteiros de acesso aleatório obviamente também terão um comprimento infinito. Portanto, se essa máquina aparecer, ela terá que oferecer um conjunto de instruções para acesso seqüencial à memória, além de uma API para idiomas de alto nível.
25916 Novil
14
@ IMil Isso não é verdade. Algumas linguagens de programação não têm limitação no espaço de endereço, nem mesmo implicitamente. Para dar um exemplo óbvio, uma máquina universal de Turing em que o estado inicial da fita forma o programa é uma linguagem de programação completa de Turing. Muitas linguagens de programação que são realmente usadas na prática têm a mesma propriedade, por exemplo, Lisp e SML. Uma linguagem não precisa ter um conceito de "ponteiro de acesso aleatório". (cont.)
Gilles 'para de ser mau'
11
@IMil (cont.) As implementações costumam ter desempenho, mas sabemos que uma implementação em execução em um computador específico não é concluída por Turing, pois é limitada pelo tamanho da memória do computador. Mas isso significa que a implementação não implementa toda a linguagem, apenas um subconjunto (de programas em execução em N bytes de memória). Você pode executar o programa em um computador e, se ficar sem memória, mova-o para um computador maior e assim por diante para sempre ou até que ele pare. Essa seria uma maneira válida de implementar toda a linguagem.
Gilles 'SO- stop be evil'
6

A adição de C99 va_copyao API de argumento variado pode nos dar uma porta dos fundos para a integridade de Turing. Como é possível iterar por meio de uma lista de argumentos variados mais de uma vez em uma função diferente daquela que originalmente recebeu os argumentos, va_argspode ser usado para implementar um ponteiro sem ponteiro.

Obviamente, uma implementação real da API de argumento variável provavelmente terá um ponteiro em algum lugar, mas em nossa máquina abstrata ela pode ser implementada usando magia.

Aqui está uma demonstração implementando um autômato de empilhamento de 2 pilhas com regras de transição arbitrárias:

#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_listfor um tipo de matriz, na verdade existem parâmetros de ponteiro ocultos para as funções. Portanto, provavelmente seria melhor alterar os tipos de todos os va_listargumentos para wrapped_stack.

feersum
fonte
Isso pode funcionar. Uma possível preocupação é que ele depende da alocação de um número ilimitado de va_listvariáveis automáticas stack. Essas variáveis ​​devem ter um endereço &stacke só podemos ter um número limitado delas. Esse requisito pode ser contornado declarando todas as variáveis ​​locais register, talvez?
chi
@chi AIUI uma variável não precisa ter um endereço, a menos que alguém tente usá-lo. Além disso, é ilegal declarar o argumento imediatamente antes das reticências register.
feersum
Pela mesma lógica, não deveria intser necessário ter um limite, a menos que alguém o use ou sizeof(int)?
chi
@chi Nem um pouco. O padrão define como parte da semântica abstrata que um inttem um valor entre alguns limites finitos INT_MINe INT_MAX. E se o valor de um intexceder o limite, ocorrerá um comportamento indefinido. Por outro lado, o padrão intencionalmente não exige que todos os objetos estejam fisicamente presentes na memória em um endereço específico, pois isso permite otimizações, como armazenar o objeto em um registro, armazenar apenas parte do objeto, representando-o de forma diferente do padrão. layout ou omiti-lo completamente, se não for necessário.
feersum
4

Aritmética fora do padrão, talvez?

Então, parece que o problema é do tamanho finito de sizeof(t). No entanto, acho que conheço uma solução alternativa.

Até onde eu sei, C não requer uma implementação para usar os números inteiros padrão para seu tipo inteiro. Portanto, poderíamos usar um modelo aritmético não-padrão . Em seguida, sizeof(t)definiríamos um número fora do padrão e agora nunca o alcançaremos em um número finito de etapas. Portanto, o comprimento da fita das máquinas de Turing sempre será menor que o "máximo", pois o máximo é literalmente impossível de alcançar. sizeof(t)simplesmente não é um número no sentido regular da palavra.

Este é um tecnicismo, é claro: o teorema de Tennenbaum . Ele afirma que o único modelo de aritmética Peano é o padrão, o que obviamente não daria. No entanto, até onde eu sei, C não exige implementações para usar tipos de dados que satisfaçam os axiomas do Peano, nem exige que a implementação seja computável, portanto isso não deve ser um problema.

O que deve acontecer se você tentar gerar um número inteiro fora do padrão? Bem, você pode representar qualquer número inteiro fora do padrão usando uma sequência não padrão, portanto, basta transmitir os dígitos da frente dessa sequência.

PyRulez
fonte
Como seria uma implementação?
reinierpost
@reinierpost Acho que representaria dados usando algum modelo não padronizado de PA. Ele calcularia operações aritméticas usando um grau de PA . Eu acho que qualquer modelo desse tipo deve fornecer uma implementação C válida.
PyRulez 28/02
Desculpe, isso não funciona. sizeof(t)em si é um valor do tipo size_t, portanto, é um número inteiro natural entre 0 e SIZE_MAX.
Gilles 'SO- stop be evil'
@Gilles SIZE_MAX também seria um padrão natural fora do padrão.
PyRulez 28/02
Esta é uma abordagem interessante. Observe que você também precisará, por exemplo, de intptr_t / uintptr_t / ptrdiff_t / intmax_t / uintmax_t para não ser padrão. Em C ++, isso causaria um grande avanço nas garantias de progresso ... não tenho certeza sobre C.
TLW
0

Na IMO, uma forte limitação é que o espaço endereçável (através do tamanho do ponteiro) é finito e isso é irrecuperável.

Alguém poderia advogar que a memória pode ser "trocada para o disco", mas em algum momento as informações do endereço excederão o tamanho endereçável.

Yves Daoust
fonte
Não é este o ponto principal da resposta aceita? Não acho que isso acrescente algo novo às respostas desta pergunta de 2016.
chi
@chi: não, a resposta aceita não menciona a troca para a memória externa, que pode ser considerada uma solução alternativa.
Yves Daoust
-1

Na prática, essas restrições são irrelevantes para a integridade de Turing. O requisito real é permitir que a fita seja arbitrária por muito tempo, não infinita. Isso criaria um problema de parada de um tipo diferente (como o universo "calcula" a fita?)

É tão falso quanto dizer "Python não é Turing completo porque você não pode fazer uma lista infinitamente grande".

[Edit: obrigado ao Sr. Whitledge por esclarecer como editar.]

o médico
fonte
7
Eu não acho que isso responda a pergunta. A pergunta já antecipou esta resposta e explicou por que ela não é válida: "embora o padrão C permita que size_t seja arbitrariamente grande, ele deve ser corrigido por algum tempo e, independentemente do tamanho que for fixado, ainda é finito. " Você tem alguma resposta a esse argumento? Acho que não podemos contar a pergunta como respondida, a menos que ela explique por que esse argumento está errado (ou certo).
DW
5
A qualquer momento, um valor do tipo size_té finito. O problema é que você não pode estabelecer um limite size_tválido para todo o cálculo: para qualquer limite, um programa pode estourá-lo. Mas a linguagem C afirma que existe um limite para size_t: em uma determinada implementação, ela só pode crescer até sizeof(size_t)bytes. Além disso, seja legal . Dizer que as pessoas que o criticam "não conseguem pensar por conta própria" é rude.
Gilles 'SO- stop be evil'
11
Essa é a resposta correta. Uma máquina de tornear não requer uma fita infinita, requer uma fita "arbitrariamente longa". Ou seja, você pode supor que a fita tenha o tempo necessário para concluir o cálculo. Você também pode presumir que o seu computador possui a quantidade de memória necessária. Uma fita infinita não é absolutamente necessária, porque qualquer cálculo que pare em tempo finito não pode usar uma quantidade infinita de fita.
Jeffrey L Whitledge
O que esta resposta mostra é que, para cada TM, você pode escrever uma implementação C com comprimento de ponteiro suficiente para simulá-la. No entanto, não é possível escrever uma implementação em C que possa simular qualquer TM. Portanto, a especificação proíbe que qualquer implementação específica seja concluída em T. Também não é o T-complete, porque o comprimento do ponteiro é fixo.
11
Essa é outra resposta correta que dificilmente é visível devido à incapacidade da maioria dos indivíduos nesta comunidade. Enquanto isso, a resposta aceita é falsa e sua seção de comentários é protegida por moderadores que excluem comentários críticos. Tchau tchau, cs.stackexchange.
xamid 7/03
-1

A mídia removível nos permite contornar o problema de memória ilimitada. Talvez as pessoas pensem que isso é um abuso, mas acho que está tudo bem e é inevitável, de qualquer maneira.

Corrija qualquer implementação de uma máquina universal de Turing. Para a fita, usamos mídia removível. Quando a cabeça sai do final ou do início do disco atual, a máquina solicita que o usuário insira o próximo ou o anterior. Podemos usar um marcador especial para indicar a extremidade esquerda da fita simulada ou ter uma fita sem limites nas duas direções.

O ponto principal aqui é que tudo o que o programa C deve fazer é finito. O computador precisa apenas de memória suficiente para simular o autômato e size_tprecisa ser grande o suficiente para permitir endereçar essa quantidade (realmente pequena) de memória e dos discos, que podem ter qualquer tamanho finito fixo. Como o usuário é solicitado apenas a inserir o disco seguinte ou anterior, não precisamos de números inteiros ilimitados para dizer "Insira o número do disco 123456 ..."

Suponho que a principal objeção provavelmente seja o envolvimento do usuário, mas isso parece inevitável em qualquer implementação, porque parece não haver outra maneira de implementar a memória ilimitada.

David Richerby
fonte
3
Eu diria que, a menos que a definição C exija esse armazenamento externo ilimitado, isso não poderá ser aceito como uma prova da integridade de Turing. (A ISO 9899 não exige que, é claro, seja escrita para a engenharia do mundo real.) O que me preocupa é que, se aceitarmos isso, por um raciocínio semelhante, poderemos afirmar que os DFAs são Turing completos, pois podem ser usados passar a cabeça sobre uma fita (o armazenamento externo).
18718 chi
@chi Não vejo como segue o argumento do DFA. O ponto principal de um DFA é que ele só tem acesso de leitura ao armazenamento. Se você permitir que "passe a cabeça sobre uma fita", não é exatamente uma máquina de Turing?
David Richerby
2
Na verdade, estou aqui um pouco. O ponto é: por que é bom adicionar uma "fita" a C, deixar C simular um DFA e usar esse fato para afirmar que C é Turing completo, quando não podemos fazer o mesmo com os DFAs? Se C não tiver como implementar a memória ilimitada por conta própria, não deverá ser considerado Turing completo. (Eu ainda chamaria de "moralmente" Turing completo, pelo menos, uma vez que os limites são tão grandes que, na prática, não importam na maioria dos casos). C (a norma ISO não é suficiente)
qui
11
@chi Tudo bem, porque C inclui rotinas de E / S de arquivos. DFAs não.
David Richerby
11
C não especifica completamente o que essas rotinas fazem - a maior parte de sua semântica é a implementação definida. A implementação de CA não é necessária para armazenar o conteúdo do arquivo, por exemplo, acho que ele pode se comportar como se todo arquivo fosse "/ dev / null", por assim dizer. Também não é necessário armazenar uma quantidade ilimitada de dados. Eu diria que seu argumento está correto, considerando o que a grande maioria das implementações de C faz e generalizando esse comportamento para uma máquina ideal. Se confiarmos estritamente na definição C, apenas esquecendo a prática, acho que ela não se aplica.
chi
-2

Escolha size_tser infinitamente grande

Você pode optar size_tpor ser infinitamente grande. Naturalmente, é impossível realizar essa implementação. Mas isso não é surpresa, dada a natureza finita do mundo em que vivemos.

Implicações práticas

Mas, mesmo que fosse possível realizar essa implementação, haveria questões práticas. Considere a seguinte instrução C:

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

SIZE_MAXSIZE_MAXO(2size_t)size_tSIZE_MAXprintf

Felizmente, para nossos propósitos teóricos, não pude encontrar nenhum requisito na especificação que garanta printfque terminará para todas as entradas. Portanto, até onde sei, não violamos a especificação C aqui.

Sobre Completude Computacional

Ainda resta provar que nossa implementação teórica é Turing Complete . Podemos mostrar isso implementando "qualquer máquina de Turing com uma única fita".

A maioria de nós provavelmente implementou uma Máquina de Turing como um projeto escolar. Não vou dar os detalhes de uma implementação específica, mas aqui está uma estratégia comumente usada:

  • O número de estados, o número de símbolos e a tabela de transição de estados são fixos para qualquer máquina. Portanto, podemos representar estados e símbolos como números e a tabela de transição de estados como uma matriz bidimensional.
  • A fita pode ser representada como uma lista vinculada. Podemos usar uma única lista com vínculo duplo ou duas listas com um vínculo único (uma para cada direção a partir da posição atual).

Agora vamos ver o que é necessário para realizar essa implementação:

  • A capacidade de representar um conjunto fixo, mas arbitrariamente grande, de números. Para representar qualquer número arbitrário, também escolhemos MAX_INTser infinitos. (Como alternativa, poderíamos usar outros objetos para representar estados e símbolos.)
  • A capacidade de construir uma lista vinculada arbitrariamente grande para nossa fita. Mais uma vez, não há limite finito no tamanho. Isso significa que não podemos construir essa lista antecipadamente, pois gastaríamos uma eternidade apenas para construir nossa fita. Porém, podemos construir essa lista incrementalmente se usarmos a alocação dinâmica de memória. Podemos usar malloc, mas há um pouco mais a considerar:
    • A especificação C permite mallocfalhar se, por exemplo, a memória disponível tiver sido esgotada. Portanto, nossa implementação é verdadeiramente universal, se mallocnunca falhar.
    • No entanto, se nossa implementação for executada em uma máquina com memória infinita, não haverá necessidade de mallocfalha. Sem violar o padrão C, nossa implementação garantirá que mallocnunca falhará.
  • A capacidade de remover referências de ponteiros, procurar elementos da matriz e acessar os membros de um nó de lista vinculado.

Portanto, a lista acima é o que é necessário para implementar uma Máquina de Turing em nossa hipotética implementação em C. Esses recursos devem terminar. Qualquer outra coisa, no entanto, pode ter permissão para não terminar (a menos que exigido pela norma). Isso inclui aritmética, E / S, etc.

Nathan Davis
fonte
6
O que seria printf("%zu\n",SIZE_MAX);impresso em tal implementação?
Ruslan
11
@Ruslan, essa implementação é impossível, assim como é impossível implementar uma máquina de Turing. Mas se tal implementação fosse possível, imagino que imprimiria alguma representação decimal de um número infinitamente grande - presumivelmente, um fluxo infinito de dígitos decimais.
Nathan Davis
2
@NathanDavis É possível implementar uma máquina de Turing. O truque é que você não precisa construir uma fita infinita - basta construir a parte usada da fita de forma incremental, conforme necessário.
Gilles 'SO- stop be evil' (
2
@ Gilles: Neste universo finito em que estamos vivendo, é impossível implementar uma máquina de Turing.
gnasher729
11
@ NathanDavis Mas se você fizer isso, você mudou sizeof(size_t)(ou CHAR_BITS). Você não pode retomar a partir do novo estado, é preciso começar novamente, mas a execução do programa pode ser diferente agora que essas constantes são diferentes
Gilles '- pare de ser mau' '
-2

O argumento principal aqui foi que o tamanho do size_t é finito, embora possa ser infinitamente grande.

Existe uma solução alternativa para isso, embora não tenha certeza se isso coincide com a ISO C.

Suponha que você tenha uma máquina com memória infinita. Portanto, você não está limitado ao tamanho do ponteiro. Você ainda tem seu tamanho size_t. Se você me perguntar o que é sizeof (size_t), a resposta será apenas sizeof (size_t). Se você perguntar se é maior que 100, por exemplo, a resposta é sim. Se você perguntar o que é sizeof (size_t) / 2, como você pode imaginar, a resposta ainda é sizeof (size_t). Se você quiser imprimi-lo, podemos concordar com alguns resultados. A diferença desses dois pode ser NaN e assim por diante.

O resumo é que relaxar a condição para size_t ter tamanho finito não interromperá nenhum programa já existente.

PS Ainda é possível alocar tamanho de memória sizeof (size_t), você só precisa de tamanho contável, então digamos que você faça todos os ajustes (ou truque semelhante).

Eugene
fonte
11
"A diferença desses dois pode ser NaN". Não, não pode ser. Não existe um NaN do tipo inteiro em C.
TLW
De acordo com o padrão, sizeoftem que retornar a size_t. Então você tem que escolher algum valor específico.
Draconis
-4

Sim.

1. Resposta citada

Como reação à grande quantidade de votos negativos nas minhas (e outras) respostas corretas - em comparação com a alarmante aprovação de respostas falsas -, procurei uma explicação alternativa menos teoricamente profunda. Eu encontrei este . Espero que abranja algumas das falácias comuns aqui, para que um pouco mais de insight seja mostrado. Parte essencial do argumento:

[...] Seu argumento é o seguinte: suponha que alguém tenha escrito um determinado programa de término que possa exigir, durante sua execução, até uma quantidade arbitrária de armazenamento. Sem alterar esse programa, é possível a posteriori implementar um hardware de computador e seu compilador C que fornecem armazenamento suficiente para acomodar esse cálculo. Isso pode exigir o alargamento das larguras de caracteres (via CHAR_BITS) e / ou ponteiros (via size_t), mas o programa não precisaria ser modificado. Como isso é possível, C é realmente Turing-Complete para finalizar programas.

A parte complicada desse argumento é que ele só funciona quando se considera o encerramento de programas. Os programas de término têm essa propriedade legal de que eles têm um limite superior estático para seus requisitos de armazenamento, que podem ser determinados experimentalmente executando o programa na entrada desejada, com tamanhos crescentes de armazenamento, até que "caiba".

A razão pela qual eu estava enganado na minha linha de pensamento é que eu estava considerando a classe mais ampla de programas não-úteis "não termináveis" [...]

Em resumo, porque para cada função computável existe uma solução na linguagem C (devido aos limites superiores ilimitados), todo problema computável possui um programa C, portanto C é Turing-completo.

2. Minha resposta original

Existem confusões generalizadas entre conceitos matemáticos na ciência da computação teórica (como a integralidade de Turing) e sua aplicação no mundo real, ou seja, técnicas em ciência da computação prática. A perfeição de Turing não é uma propriedade de máquinas fisicamente existentes ou de qualquer modelo limitado no espaço-tempo. É apenas um objeto abstrato que descreve propriedades de teorias matemáticas.

O C99 é Turing-complete, independentemente das restrições baseadas na implementação, como praticamente qualquer outra linguagem de programação comum, pois é capaz de expressar um conjunto funcionalmente completo de conectivos lógicos e, em princípio, tem acesso a uma quantidade ilimitada de memória. As pessoas apontaram que C restringe explicitamente o acesso à memória aleatória para ser limitado, mas isso não é algo que não possa ser contornado, uma vez que essas são restrições adicionais declaradas no padrão C, enquanto a integridade de Turing é necessária sem elas :

Aqui está uma coisa muito básica sobre sistemas lógicos que deve ser suficiente para uma prova não construtiva . Considere um cálculo com alguns esquemas e regras de axioma, de modo que o conjunto de conseqüências lógicas seja X. Agora, se você adicionar algumas regras ou axiomas, o conjunto de consequências lógicas aumentará, ou seja, deve ser um superconjunto de X. É por isso que, por exemplo, , a lógica modal S4 está adequadamente contida em S5. Da mesma forma, quando você tem uma subespecificação completa de Turing, mas adiciona algumas restrições na parte superior, elas não impedem nenhuma das conseqüências em X, ou seja, deve haver uma maneira de contornar todas as restrições. Se você deseja um idioma não completo de Turing, o cálculo deve ser reduzido, não estendido. Extensões que afirmam que algo não seria possível, mas na verdade são, apenas adicionam inconsistência. Essas inconsistências no padrão C podem não ter conseqüências práticas, assim como a integridade de Turing não está relacionada à aplicação prática.

Simulando números arbitrários baseado em profundidade de recursão (ou seja, este , com a possibilidade de apoiar vários números através de agendamento / pseudo-threads; não há limite teórico para a profundidade de recursão em C ), ou usando o armazenamento de arquivos para simular memória programa ilimitado ( ideia ) são provavelmente apenas duas dentre infinitas possibilidades para provar construtivamente a completude de Turing de C99. É preciso lembrar que, para computabilidade, a complexidade do tempo e do espaço é irrelevante. Em particular, assumir um ambiente limitado para falsificar a completude de Turing é apenas um raciocínio circular, uma vez que essa limitação exclui todos os problemas que excedem o limite de complexidade pressuposto.

( NOTA : Eu apenas escrevi esta resposta para impedir que as pessoas sejam impedidas de obter intuição matemática devido a algum tipo de pensamento limitado orientado a aplicativos. É uma pena que a maioria dos alunos leia a resposta falsa aceita por ser votada com base em falhas fundamentais de raciocínio, para que mais pessoas espalhem essas falsas crenças. Se você rejeitar esta resposta, você será apenas parte do problema.)

xamid
fonte
4
Eu não sigo o seu último parágrafo. Você alega que adicionar restrições aumenta o poder expressivo, mas isso claramente não é verdade. Restrições só podem diminuir o poder expressivo. Por exemplo, se você pegar C e adicionar a restrição de que nenhum programa pode acessar mais do que 640kb de armazenamento (de qualquer tipo), transformou-o em um autômato finito sofisticado que claramente não é completo em Turing.
David Richerby
3
Se você tiver uma quantidade fixa de armazenamento, não poderá simular nada que exija mais recursos do que possui. Existem apenas finitas configurações em que sua memória pode estar, o que significa que existem apenas finitamente muitas coisas que você pode fazer.
David Richerby
2
Não entendo por que você se refere a "máquinas fisicamente existentes". Observe que Turing-completeness é uma propriedade de um modelo computacional matemático, não de sistemas físicos. Concordo que nenhum sistema físico, sendo um objeto finito, pode se aproximar do poder das máquinas de Turing, mas isso é irrelevante. Ainda podemos pegar qualquer linguagem de programação, considerar a definição matemática de sua semântica e verificar se esse objeto matemático está completo de Turing. O jogo da vida de Conway é Turing poderoso, mesmo que não haja uma implementação física possível.
chi
2
@xamid Se você tiver dúvidas sobre as políticas de moderação deste site, acesse o Computer Science Meta . Até lá, por favor, seja legal . O abuso verbal de outras pessoas não será tolerado. (Eu removi todos os comentários que não pertencem ao assunto em questão.)
Raphael
2
Você diz que modificar a largura de um ponteiro não mudaria o programa, mas os programas podem ler a largura de um ponteiro e fazer o que quiserem com esse valor. O mesmo para CHAR_BITS.
Draconis