Por que é impossível construir um compilador que possa determinar se uma função C ++ mudará o valor de uma determinada variável?

104

Eu li esta linha em um livro:

É provavelmente impossível construir um compilador que possa realmente determinar se uma função C ++ mudará ou não o valor de uma determinada variável.

O parágrafo estava falando sobre por que o compilador é conservador ao verificar a constância.

Por que é impossível construir um compilador assim?

O compilador sempre pode verificar se uma variável é reatribuída, uma função não const está sendo invocada nela ou se está sendo passada como um parâmetro não const ...

Jogador de críquete
fonte
24
A primeira coisa que me vem à mente são as bibliotecas de links dinâmicos. Se eu compilar o código na minha máquina e você compilar o código na sua máquina e nós os vincularmos em tempo de execução , como o seu compilador poderia saber se eu modifiquei as variáveis ​​ou não?
Mooing Duck
4
@MooingDuck Exatamente isso. De forma mais ampla, o compilador não compila a função individualmente, mas a compila como parte de uma imagem mais ampla que pode não estar totalmente dentro do escopo do compilador.
chamado 2voyage em
3
"impossível" pode ser um exagero - "computacionalmente inviável" (como em NP-difícil) pode ser uma caracterização melhor, mas é um pouco mais difícil para o aluno entender. Imagine uma lista vinculada ou outra estrutura de dados abstrata. Se eu chamar uma função que muda um nó nessa lista / árvore / qualquer coisa, como um compilador poderia esperar provar exatamente qual nó foi modificado (e talvez mais importante, quais não foram) sem basicamente simular totalmente o programa com o entrada esperada, embora não
leve
36
@twalberg Impossível não é um exagero, o problema do Halting se aplica aqui, conforme explicam várias respostas. Simplesmente não é possível analisar completamente um programa geral por meio de algoritmos.
Fiktik 01 de
5
@twalberg Compiladores que compilam apenas um subconjunto de programas válidos não são muito úteis.
Caleb

Respostas:

139

Por que é impossível construir um compilador assim?

Pela mesma razão que você não pode escrever um programa que irá determinar se um determinado programa será encerrado. Isso é conhecido como o problema da parada e é uma daquelas coisas que não são computáveis.

Para ser claro, você pode escrever um compilador que pode determinar se uma função muda a variável em alguns casos , mas você não pode escrever um que diga com segurança que a função irá ou não alterar a variável (ou interromper) para todas as funções possíveis.

Aqui está um exemplo fácil:

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

Como um compilador pode determinar, apenas olhando para aquele código, se fooalguma vez mudará a? Se faz ou não depende de condições externas à função, nomeadamente a implementação de bar. Há mais do que isso para provar que o problema da parada não é computável, mas já está bem explicado no artigo da Wikipedia (e em todos os livros de teoria da computação), então não tentarei explicá-lo corretamente aqui.

Caleb
fonte
48
@mrsoltys, os computadores quânticos são "apenas" exponencialmente mais rápidos para alguns problemas, eles não podem resolver problemas indecidíveis.
zch 01 de
8
@mrsoltys Esses algoritmos exponencialmente complicados (como fatoração) são perfeitos para computadores quânticos, mas o problema de parada é um dilema lógico, não é computável, não importa que tipo de "computador" você tenha.
user1032613
7
@mrsoltys, só para ser um espertinho, sim, mudaria. Infelizmente, isso significaria que o algoritmo foi encerrado e ainda está em execução, infelizmente, você não pode dizer qual sem observar diretamente, pelo qual você afeta o estado real.
Nathan Ernst
9
@ ThorbjørnRavnAndersen: OK, então, suponha que eu esteja executando um programa. Como exatamente posso determinar se ele será encerrado?
ruakh 01 de
8
@ ThorbjørnRavnAndersen Mas se você realmente executar o programa e ele não terminar (por exemplo, um loop infinito), você nunca descobrirá que ele não termina ... você apenas continua executando mais uma etapa, porque pode ser o último ...
MaxAxeHax 01 de
124

Imagine que tal compilador existe. Vamos supor também que, por conveniência, ele fornece uma função de biblioteca que retorna 1 se a função passada modificar uma determinada variável e 0 quando a função não modifica. Então o que este programa deve imprimir?

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;
}
orlp
fonte
12
Agradável! O paradoxo Eu sou um mentiroso , escrito por um programador.
Krumelur 01 de
28
Na verdade, é apenas uma boa adaptação da famosa prova da indecidibilidade do problema da parada .
Konstantin Weitz
10
Neste caso concreto, "modifies_variable" deve retornar verdadeiro: Há pelo menos um caminho de execução no qual a variável é realmente modificada. E esse caminho de execução é alcançado após uma chamada para uma função externa não determinística - portanto, toda a função é não determinística. Por esses 2 motivos, o compilador deve assumir a visão pesimística e decidir que modifica a variável. Se o caminho para modificar a variável for alcançado após uma comparação determinística (verificável pelo compilador) resultar em falso (ou seja, "1 == 1"), então o compilador poderia dizer com segurança que tal função nunca modifica a variável
Joe Pineda
6
@JoePineda: A questão é se fmodifica a variável - não se poderia modificar a variável. Esta resposta está correta.
Neil G
4
@JoePineda: nada me impede de copiar / colar o código do modifies_variablefonte do compilador, anulando totalmente o seu argumento. (assumindo código aberto, mas o ponto deve ser claro)
orlp
60

Não confunda "irá ou não modificar uma variável dada essas entradas" para "tem um caminho de execução que modifica uma variável."

O primeiro é chamado de determinação de predicado opaco e é trivialmente impossível de decidir - além da redução do problema de parada, você pode apenas apontar que as entradas podem vir de uma fonte desconhecida (por exemplo, o usuário). Isso é verdade para todas as linguagens, não apenas C ++.

A última instrução, entretanto, pode ser determinada olhando a árvore de análise, que é algo que todos os compiladores de otimização fazem. A razão disso é que funções puras (e funções referencialmente transparentes , para alguma definição de referencialmente transparente ) têm todos os tipos de ótimas otimizações que podem ser aplicadas, como serem facilmente embutidas ou ter seus valores determinados em tempo de compilação; mas para saber se uma função é pura, precisamos saber se ela pode modificar uma variável.

Portanto, o que parece ser uma declaração surpreendente sobre C ++ é, na verdade, uma declaração trivial sobre todas as linguagens.

BlueRaja - Danny Pflughoeft
fonte
5
Esta é a melhor resposta, imho, é importante fazer essa distinção.
UncleZeiv
"trivialmente impossível"?
Kip
2
@Kip "trivialmente impossível de decidir" provavelmente significa "impossível de decidir, e a prova é trivial".
fredoverflow
28

Acho que a palavra-chave em "se uma função C ++ irá ou não alterar o valor de uma variável específica" é "irá". Certamente é possível construir um compilador que verifica se uma função C ++ tem permissão para alterar o valor de uma variável específica, você não pode dizer com certeza se a mudança vai acontecer:

void maybe(int& val) {
    cout << "Should I change value? [Y/N] >";
    string reply;
    cin >> reply;
    if (reply == "Y") {
        val = 42;
    }
}
dasblinkenlight
fonte
“Certamente é possível construir um compilador que verifique se uma função C ++ pode ou não alterar o valor de uma determinada variável” Não, não é. Veja a resposta de Caleb. Para um compilador saber se foo () pode alterar a, ele teria que saber se é possível para bar () retornar 0. E não há função computável que possa dizer todos os valores de retorno possíveis de qualquer função computável. Portanto, existem caminhos de código de forma que o compilador não será capaz de dizer se eles serão alcançados. Se uma variável for alterada apenas em um caminho de código que não pode ser alcançado, ela não mudará, mas um compilador não a detectará
Martin Epsz
12
@MartinEpsz Por "pode" quero dizer "tem permissão para mudar", não "pode ​​possivelmente mudar". Eu acredito que isso é o que OP tinha em mente ao falar sobre constverificações de -ness.
dasblinkenlight
@dasblinkenlight Eu teria que concordar que acredito que o OP pode significar o primeiro, "é permitido mudar", ou "pode ​​ou não mudar" vs. "definitivamente não mudará". É claro que não consigo pensar em um cenário em que isso seja um problema. Você pode até mesmo modificar o compilador para simplesmente responder "pode ​​mudar" em qualquer função que contenha o identificador ou uma chamada para uma função que tenha um atributo de resposta "pode ​​mudar". Dito isso, C e C ++ são linguagens horríveis para tentar fazer isso, já que têm uma definição tão vaga das coisas. Eu acho que é por isso que constância seria um problema em C ++.
DDS
@MartinEpsz: "E não há função computável que possa dizer todos os valores de retorno possíveis de qualquer função computável". Acho que verificar "todos os valores de retorno possíveis" é uma abordagem incorreta. Existem sistemas matemáticos (maxima, mathlab) que podem resolver equações, o que significa que faria sentido aplicar uma abordagem semelhante às funções. Ou seja, tratá-lo como uma equação com várias incógnitas. Os problemas são controle de fluxo + efeitos colaterais => situações insolúveis. IMO, sem eles (linguagem funcional, sem atribuição / efeitos colaterais), seria possível prever qual caminho o programa tomará
SigTerm
16

Não acho que seja necessário invocar o problema da parada para explicar que você não pode saber algoritmicamente em tempo de compilação se uma determinada função modificará uma determinada variável ou não.

Em vez disso, é suficiente apontar que o comportamento de uma função geralmente depende de condições de tempo de execução, as quais o compilador não pode saber com antecedência. Por exemplo

int y;

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

Como o compilador poderia prever com certeza se yserá modificado?

LarsH
fonte
7

Isso pode ser feito e os compiladores estão fazendo isso o tempo todo para algumas funções ; esta é, por exemplo, uma otimização trivial para acessadores embutidos simples ou muitas funções puras.

O que é impossível é saber no caso geral.

Sempre que houver uma chamada de sistema ou uma chamada de função vinda de outro módulo, ou uma chamada para um método potencialmente sobrescrito, qualquer coisa pode acontecer, incluindo o controle hostil do uso de algum hacker de estouro de pilha para alterar uma variável não relacionada.

No entanto, você deve usar const, evitar globais, preferir referências a ponteiros, evitar reutilizar variáveis ​​para tarefas não relacionadas, etc., o que tornará a vida do compilador mais fácil ao realizar otimizações agressivas.

Kriss
fonte
1
Se bem me lembro, esse é o ponto principal da programação funcional, certo? Usando apenas funções puramente determinísticas, sem efeitos colaterais, os compiladores são livres para fazer otimizações agressivas, pré-execução, pós-execução, memoização e até mesmo execução em tempo de compilação. O ponto que eu acho que muitos respondentes estão ignorando (ou confusos) é que isso é realmente possível para um subconjunto bem-comportado de todos os programas . E não, esse subconjunto não é trivial ou desinteressante, na verdade é muito útil. Mas é realmente impossível para o caso geral absoluto.
Joe Pineda
A sobrecarga é um conceito de tempo de compilação. Você provavelmente quis dizer "método substituído".
fredoverflow
@FredOverflow: sim, quero dizer substituído. A sobrecarga é de fato um conceito de tempo de compilação. Obrigado por identificá-lo (é claro que se a implementação vier de outra unidade de compilação, o compilador ainda pode ter problemas para analisá-lo, mas não foi isso que eu quis dizer). Vou consertar a resposta.
kriss
6

Existem várias maneiras de explicar isso, uma das quais é o problema da parada :

Na teoria da computabilidade, o problema da parada pode ser enunciado da seguinte maneira: "Dada uma descrição de um programa de computador arbitrário, decida se o programa termina de ser executado ou continua a ser executado para sempre". Isso é equivalente ao problema de decidir, dado um programa e uma entrada, se o programa irá eventualmente parar quando executado com aquela entrada ou se executará para sempre.

Alan Turing provou em 1936 que um algoritmo geral para resolver o problema de parada para todos os pares de entrada de programa possíveis não pode existir.

Se eu escrever um programa parecido com este:

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

O valor da xmudança? Para determinar isso, primeiro você teria que determinar se a do tons of complex stuffpeça causa o disparo da condição - ou ainda mais básico, se ela para. Isso é algo que o compilador não pode fazer.

Escudos Timothy
fonte
6

Realmente surpreso que não haja uma resposta para usar o problema da parada diretamente! Há uma redução muito direta desse problema para o problema da parada.

Imagine que o compilador pudesse dizer se uma função alterou ou não o valor de uma variável. Então, certamente seria capaz de dizer se a função a seguir altera o valor de y ou não, assumindo que o valor de x pode ser rastreado em todas as chamadas ao longo do restante do programa:

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

Agora, para qualquer programa de que gostamos, vamos reescrevê-lo como:

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

Observe que, se, e somente se, nosso programa alterar o valor de y, ele então termina - foo () é a última coisa que ele faz antes de sair. Isso significa que resolvemos o problema da parada!

O que a redução acima nos mostra é que o problema de determinar se o valor de uma variável muda é pelo menos tão difícil quanto o problema da parada. O problema da parada é conhecido por ser incomputável, então este também deve ser.

John Doucette
fonte
Não tenho certeza se estou seguindo seu raciocínio sobre por que nosso programa é encerrado se alterar o valor de y. Parece-me que foo()volta rapidamente e depois main()sai. (Além disso, você está ligando foo()sem uma discussão ... isso é parte da minha confusão.)
LarsH
1
@LarsH: Se o programa modificado terminar, a última função que ele chamou foi f. Se y foi modificado, f foi chamado (as outras instruções não podem alterar y, pois ele foi introduzido apenas pela modificação). Portanto, se y for modificado, o programa termina.
MSalters
4

Assim que uma função chama outra função da qual o compilador não "vê" a origem, ele deve assumir que a variável foi alterada ou as coisas podem dar errado mais adiante. Por exemplo, digamos que temos isso em "foo.cpp":

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

e temos isso em "bar.cpp":

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

Como o compilador pode "saber" que xnão está mudando (ou ESTÁ mudando, mais apropriadamente) bar?

Tenho certeza de que podemos chegar a algo mais complexo, se isso não for complexo o suficiente.

Mats Petersson
fonte
O compilador pode saber que x não está mudando na barra se a barra x for passada como passagem por referência para const, certo?
Jogador de críquete de
Sim, mas se eu adicionar um const_cast em foo, ainda faria a xmudança - eu estaria violando o contrato que diz que você não deve alterar as constvariáveis, mas uma vez que você pode converter qualquer coisa para "mais const", e const_castexiste, os designers da linguagem certamente tinham a ideia de que às vezes há boas razões para acreditar que os constvalores podem precisar de mudança.
Mats Petersson
@MatsPetersson: Eu acredito que se você const_cast consegue manter todas as partes que quebram porque o compilador pode, mas não tem que compensar isso.
Zan Lynx
@ZanLynx: Sim, tenho certeza que está correto. Mas, ao mesmo tempo, o elenco existe, o que significa que alguém que projetou a linguagem teve algum tipo de ideia de que "podemos precisar disso em algum momento" - o que significa que não foi feito para não fazer nada útil.
Mats Petersson
1

Em geral, é impossível para o compilador determinar se a variável será alterada, como já foi apontado.

Ao verificar a constância, a questão de interesse parece ser se a variável pode ser alterada por uma função. Mesmo isso é difícil em linguagens que suportam ponteiros. Você não pode controlar o que outro código faz com um ponteiro, ele pode até ser lido de uma fonte externa (embora improvável). Em linguagens que restringem o acesso à memória, esses tipos de garantias podem ser possíveis e permitem uma otimização mais agressiva do que o C ++.

Krumelur
fonte
2
Uma coisa que eu gostaria que fosse suportada em linguagens seria uma distinção entre referências (ou ponteiros) efêmeras, retornáveis ​​e persistentes. As referências efêmeras só podem ser copiadas para outras referências efêmeras, as retornáveis ​​podem ser copiadas para as efêmeras ou retornáveis ​​e as persistentes podem ser copiadas de qualquer maneira. O valor de retorno de uma função será restringido pelo mais restritivo dos argumentos que são passados ​​como parâmetros "retornáveis". Considero lamentável que em muitas línguas, quando se passa uma referência, não há nada que indique por quanto tempo ela pode ser usada.
supercat
Isso certamente seria útil. Claro que existem padrões para isso, mas em C ++ (e muitas outras linguagens) é sempre possível "trapacear".
Krumelur 01 de
Uma das principais formas em que o .NET é superior ao Java é que ele tem um conceito de referência efêmera, mas infelizmente não há como os objetos expor propriedades como referências efêmeras (o que eu realmente gostaria de ver seria um meio por qual código usando uma propriedade passaria uma referência efêmera para um código (junto com variáveis ​​temporárias) que deveria ser usado para manipular o objeto.
supercat
1

Para tornar a pergunta mais específica, sugiro que o seguinte conjunto de restrições pode ter sido o que o autor do livro pode ter tido em mente:

  1. Suponha que o compilador esteja examinando o comportamento de uma função específica em relação à constância de uma variável. Para correção, um compilador teria que assumir (por causa do aliasing conforme explicado abaixo) se a função chamada de outra função a variável foi alterada, então a suposição nº 1 se aplica apenas a fragmentos de código que não fazem chamadas de função.
  2. Suponha que a variável não seja modificada por uma atividade assíncrona ou simultânea.
  3. Suponha que o compilador está apenas determinando se a variável pode ser modificada, não se ela será modificada. Em outras palavras, o compilador está apenas realizando análises estáticas.
  4. Suponha que o compilador está apenas considerando o código que funciona corretamente (sem considerar saturação / subexecução de array, ponteiros ruins, etc.)

No contexto do projeto do compilador, acho que as suposições 1,3,4 fazem sentido na visão de um redator do compilador no contexto de correção de geração de código e / ou otimização de código. A suposição 2 faz sentido na ausência da palavra-chave volátil. E essas suposições também focam a questão o suficiente para fazer o julgamento de uma resposta proposta muito mais definitiva :-)

Dadas essas suposições, uma razão principal pela qual a constância não pode ser assumida é devido ao aliasing de variável. O compilador não pode saber se outra variável aponta para a variável const. O aliasing pode ser devido a outra função na mesma unidade de compilação, caso em que o compilador pode examinar as funções e usar uma árvore de chamada para determinar estaticamente que o aliasing pode ocorrer. Mas se o aliasing é devido a uma biblioteca ou outro código estrangeiro, o compilador não tem como saber na entrada da função se as variáveis ​​têm alias.

Você pode argumentar que, se uma variável / argumento for marcada como const, ela não deve estar sujeita a alterações por meio de apelidos, mas para um redator de compilador isso é muito arriscado. Pode até ser arriscado para um programador humano declarar uma variável const como parte de, digamos, um grande projeto onde ele não conhece o comportamento de todo o sistema, ou do sistema operacional, ou de uma biblioteca, para realmente conhecer uma variável. t mudar.

Χpẘ
fonte
0

Mesmo se uma variável for declarada const, não significa que algum código mal escrito possa sobrescrevê-la.

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

resultado:

1
2
Mark Lakata
fonte
Isso acontece porque ae bsão variáveis ​​de pilha e, por b[1]acaso, estão no mesmo local de memória que a.
Mark Lakata 01 de
1
-1. Comportamento indefinido remove todas as restrições ao comportamento do compilador.
MSalters
Não tenho certeza sobre o voto negativo. Este é apenas um exemplo que vai à pergunta original do OP sobre por que um compilador não consegue descobrir se algo é verdadeiro constse tudo está rotulado const. É porque o comportamento indefinido faz parte do C / C ++. Eu estava tentando encontrar uma maneira diferente de responder à sua pergunta, em vez de mencionar o problema da parada ou intervenção humana externa.
Mark Lakata
0

Para expandir meus comentários, o texto desse livro não é claro, o que ofusca a questão.

Como comentei, esse livro está tentando dizer: "vamos obter um número infinito de macacos para escrever todas as funções C ++ concebíveis que possam ser escritas. Haverá casos em que se escolhermos uma variável que (alguma função particular que os macacos escreveram) usa, não podemos descobrir se a função irá alterar essa variável. "

É claro que, para algumas (até mesmo muitas) funções em qualquer aplicativo, isso pode ser determinado pelo compilador e muito facilmente. Mas não para todos (ou necessariamente para a maioria).

Esta função pode ser facilmente analisada:

static int global;

void foo()
{
}

"foo" claramente não modifica "global". Ele não modifica absolutamente nada, e um compilador pode resolver isso facilmente.

Esta função não pode ser analisada desta forma:

static int global;

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

Uma vez que as ações de "foo" dependem de um valor que pode mudar em tempo de execução , ele patentemente não pode ser determinado em tempo de compilação se irá modificar "global".

Todo esse conceito é muito mais simples de entender do que os cientistas da computação fazem parecer. Se a função pode fazer algo diferente com base em coisas que podem mudar no tempo de execução, então você não pode descobrir o que ela fará até que seja executada, e cada vez que for executada, pode fazer algo diferente. Independentemente de ser impossível ou não, é obviamente impossível.

El Zorko
fonte
o que você diz é verdade, mas mesmo para programas muito simples, para os quais tudo se sabe em tempo de compilação, você não poderá provar nada, nem mesmo que o programa pare. Este é o problema da parada. Por exemplo, você poderia escrever um programa baseado em Hailstone Sequences en.wikipedia.org/wiki/Collatz_conjecture e fazê-lo retornar verdadeiro se convergir para um. Os compiladores não serão capazes de fazer isso (pois em muitos casos estouraria) e mesmo os matemáticos não sabem se é verdade ou não.
Kriss
Se você quer dizer "existem alguns programas de aparência muito simples para os quais você não pode provar nada", concordo inteiramente. Mas a clássica prova do Problema da Halting de Turing se baseia essencialmente em um programa em si ser capaz de dizer se ele para para estabelecer uma contradição. Como isso é matemática, não implementação. Certamente existem programas em que é inteiramente possível determinar estaticamente em tempo de compilação se uma determinada variável será modificada e se o programa será interrompido. Pode não ser matematicamente provável, mas é praticamente alcançável em certos casos.
El Zorko