¿Por qué es imposible construir un compilador que pueda determinar si una función de C ++ cambiará el valor de una variable en particular?

104

Leí esta línea en un libro:

Probablemente es imposible construir un compilador que realmente pueda determinar si una función de C ++ cambiará o no el valor de una variable en particular.

El párrafo hablaba de por qué el compilador es conservador al verificar la consistencia.

¿Por qué es imposible construir tal compilador?

El compilador siempre puede comprobar si se reasigna una variable, si se invoca una función no constante en ella o si se pasa como un parámetro no constante ...

Jugador de críquet
fuente
24
Lo primero que me viene a la mente son las bibliotecas de enlaces dinámicos. Si compilo código en mi máquina, y usted compila código en su máquina, y los enlazamos en tiempo de ejecución , ¿cómo podría saber su compilador si modifiqué las variables o no?
Mooing Duck
4
@MooingDuck Exactamente esto. En términos más generales, el compilador no compila la función individualmente, sino que la compila como parte de una imagen más amplia que puede no estar dentro del alcance del compilador.
called2voyage
3
"imposible" puede ser una exageración - "computacionalmente inviable" (como en NP-hard) puede ser una mejor caracterización, pero es un poco más difícil de entender para el estudiante. Imagínese una lista enlazada u otra estructura de datos abstracta. Si llamo a una función que cambia un nodo en esa lista / árbol / lo que sea, ¿cómo podría un compilador esperar probar exactamente qué nodo se modificó (y quizás lo más importante, cuáles no) sin básicamente simular completamente el programa con el entrada esperada, todo sin tomar 3 días para compilar un archivo fuente ...
twalberg
36
@twalberg Impossible no es una exageración, el problema de la detención se aplica aquí, como explican varias respuestas. Simplemente no es posible analizar algorítmicamente un programa general.
Fiktik
5
@twalberg Los compiladores que solo compilan un subconjunto de programas válidos no son muy útiles.
Caleb

Respuestas:

139

¿Por qué es imposible construir tal compilador?

Por la misma razón que no puede escribir un programa que determine si un programa dado terminará. Esto se conoce como el problema de la detención y es una de esas cosas que no se pueden calcular.

Para ser claros, puede escribir un compilador que pueda determinar que una función cambia la variable en algunos casos , pero no puede escribir uno que le diga de manera confiable que la función cambiará o no la variable (o se detendrá) por todas las funciones posibles.

Aquí tienes un ejemplo sencillo:

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

¿Cómo puede un compilador determinar, con solo mirar ese código, si fooalguna vez cambiará a? Si lo hace o no depende de condiciones externas a la función, es decir, la implementación de bar. Hay más que eso en la prueba de que el problema de la detención no es computable, pero ya está bien explicado en el artículo de Wikipedia vinculado (y en todos los libros de texto de teoría de la computación), así que no intentaré explicarlo correctamente aquí.

Caleb
fuente
48
@mrsoltys, las computadoras cuánticas son "sólo" exponencialmente más rápidas para algunos problemas, no pueden resolver problemas indecidibles.
zch
8
@mrsoltys Esos algoritmos exponencialmente complicados (como la factorización) son perfectos para las computadoras cuánticas, pero detener el problema es un dilema lógico, no es computable sin importar qué tipo de "computadora" tenga.
user1032613
7
@mrsoltys, solo para ser un listillo, sí, cambiaría. Desafortunadamente, significaría que el algoritmo está terminado y aún se está ejecutando, desafortunadamente, no puede saber cuál sin observar directamente, por lo que afecta el estado real.
Nathan Ernst
9
@ ThorbjørnRavnAndersen: Bien, supongamos que estoy ejecutando un programa. ¿Cómo puedo determinar exactamente si terminará?
ruakh
8
@ ThorbjørnRavnAndersen Pero si realmente ejecuta el programa y no termina (por ejemplo, un bucle infinito), nunca descubrirá que no termina ... simplemente siga ejecutando un paso más, porque podría ser el último ...
MaxAxeHax
124

Imagínese que tal compilador existe. Supongamos también que, por conveniencia, proporciona una función de biblioteca que devuelve 1 si la función pasada modifica una variable dada y 0 cuando la función no lo hace. Entonces, ¿qué debería imprimir este programa?

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
fuente
12
¡Agradable! La paradoja de Soy un mentiroso escrita por un programador.
Krumelur
28
En realidad, es solo una buena adaptación de la famosa prueba de indecidibilidad del problema de la detención .
Konstantin Weitz
10
En este caso concreto, "modifica_variable" debería devolver verdadero: hay al menos una ruta de ejecución en la que la variable se modifica. Y esa ruta de ejecución se alcanza después de una llamada a una función externa no determinista, por lo que la función completa no es determinista. Por estas 2 razones, el compilador debería adoptar una visión pesimista y decidir si modifica la variable. Si la ruta para modificar la variable se alcanza después de que una comparación determinista (verificable por el compilador) arroja falso (es decir, "1 == 1"), el compilador podría decir con seguridad que dicha función nunca modifica la variable
Joe Pineda
6
@JoePineda: La pregunta es si fmodifica la variable, no si podría modificar la variable. Esta respuesta es correcta.
Neil G
4
@JoePineda: nada me impide copiar / pegar el código de modifies_variablela fuente del compilador, anulando totalmente su argumento. (suponiendo que sea de código abierto, pero el punto debe ser claro)
orlp
60

No confunda "modificará o no una variable dadas estas entradas" porque "tiene una ruta de ejecución que modifica una variable".

El primero se denomina determinación de predicado opaco y es trivialmente imposible de decidir; aparte de la reducción del problema de la detención, puede señalar que las entradas pueden provenir de una fuente desconocida (por ejemplo, el usuario). Esto es cierto para todos los lenguajes, no solo C ++.

Sin embargo, la última declaración se puede determinar mirando el árbol de análisis, que es algo que hacen todos los compiladores de optimización. La razón por la que lo hacen es que las funciones puras (y las funciones referencialmente transparentes , para alguna definición de referencialmente transparente ) tienen todo tipo de optimizaciones agradables que se pueden aplicar, como ser fácilmente insertables o tener sus valores determinados en tiempo de compilación; pero para saber si una función es pura, necesitamos saber si alguna vez podrá modificar una variable.

Entonces, lo que parece ser una declaración sorprendente sobre C ++ es en realidad una declaración trivial sobre todos los lenguajes.

BlueRaja - Danny Pflughoeft
fuente
5
Esta es la mejor respuesta en mi humilde opinión, es importante hacer esa distinción.
UncleZeiv
"trivialmente imposible"?
Kip
2
@Kip "trivialmente imposible de decidir" probablemente significa "imposible de decidir, y la prueba es trivial".
fredoverflow
28

Creo que la palabra clave en "si una función de C ++ cambiará o no el valor de una variable en particular" es "voluntad". Ciertamente es posible construir un compilador que verifique si una función de C ++ puede cambiar el valor de una variable en particular, no se puede decir con certeza que el cambio va a ocurrir:

void maybe(int& val) {
    cout << "Should I change value? [Y/N] >";
    string reply;
    cin >> reply;
    if (reply == "Y") {
        val = 42;
    }
}
dasblinkenlight
fuente
"Ciertamente es posible construir un compilador que verifique si una función de C ++ puede cambiar el valor de una variable en particular" No, no lo es. Vea la respuesta de Caleb. Para que un compilador sepa si foo () puede cambiar a, debería saber si es posible que bar () devuelva 0. Y no hay una función computable que pueda decir todos los valores de retorno posibles de cualquier función computable. Entonces existen rutas de código de modo que el compilador no podrá decir si alguna vez se alcanzarán. Si una variable se cambia solo en una ruta de código que no se puede alcanzar, no cambiará, pero un compilador no la detectará
Martin Epsz
12
@MartinEpsz Por "puede" me refiero a "se le permite cambiar", no "posiblemente puede cambiar". Creo que esto es lo que OP tenía en mente cuando hablaba de constcontroles de estado.
dasblinkenlight
@dasblinkenlight Tendría que estar de acuerdo en que creo que el OP puede haber significado el primero, "se permite o cambiar", o "puede o no puede cambiar" frente a "definitivamente no cambiará". Por supuesto, no puedo pensar en un escenario en el que esto sea un problema. Incluso podría modificar el compilador para que simplemente responda "puede cambiar" en cualquier función que contenga el identificador o una llamada a una función que tenga un atributo de respuesta "puede cambiar". Dicho esto, C y C ++ son lenguajes horribles con los que probar esto, ya que tienen una definición tan imprecisa de las cosas. Creo que esta es la razón por la que la const-ness sería un problema en C ++.
DDS
@MartinEpsz: "Y no hay una función computable que pueda decir todos los posibles valores de retorno de cualquier función computable". Creo que comprobar "todos los valores de retorno posibles" es un enfoque incorrecto. Hay sistemas matemáticos (maxima, mathlab) que pueden resolver ecuaciones, lo que significa que tendría sentido aplicar un enfoque similar a las funciones. Es decir, trátelo como una ecuación con varias incógnitas. Los problemas son control de flujo + efectos secundarios => situaciones sin solución. En mi opinión, sin esos (lenguaje funcional, sin asignación / efectos secundarios), habría sido posible predecir qué camino tomará el programa
SigTerm
16

No creo que sea necesario invocar el problema de la detención para explicar que no se puede saber algorítmicamente en el momento de la compilación si una función determinada modificará una determinada variable o no.

En cambio, es suficiente señalar que el comportamiento de una función a menudo depende de las condiciones de tiempo de ejecución, que el compilador no puede conocer de antemano. P.ej

int y;

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

¿Cómo podría el compilador predecir con certeza si yse modificará?

LarsH
fuente
7

Se puede hacer y los compiladores lo hacen todo el tiempo para algunas funciones , esto es, por ejemplo, una optimización trivial para accesos en línea simples o muchas funciones puras.

Lo que es imposible es conocerlo en el caso general.

Siempre que haya una llamada al sistema o una llamada a una función proveniente de otro módulo, o una llamada a un método potencialmente anulado, cualquier cosa podría suceder, incluida la toma de control hostil por el uso de un hacker de un desbordamiento de pila para cambiar una variable no relacionada.

Sin embargo, debe usar const, evitar globales, preferir referencias a punteros, evitar reutilizar variables para tareas no relacionadas, etc., lo que facilitará la vida del compilador al realizar optimizaciones agresivas.

kriss
fuente
1
Si lo recuerdo correctamente, ese es el objetivo de la programación funcional, ¿verdad? Mediante el uso de funciones puramente deterministas, sin efectos secundarios, los compiladores son libres de realizar optimizaciones agresivas, ejecución previa, ejecución posterior, memorización e incluso ejecución en tiempo de compilación. El punto que creo que muchos de los que responden ignoran (o confunden) es que, de hecho, es posible que un subconjunto de todos los programas se comporte bien . Y no, este subconjunto no es trivial ni poco interesante, en realidad es muy útil. Pero de hecho es imposible para el caso general absoluto.
Joe Pineda
La sobrecarga es un concepto en tiempo de compilación. Probablemente quisiste decir "método anulado".
fredoverflow
@FredOverflow: sí, me refiero a anulado. La sobrecarga es de hecho un concepto de tiempo de compilación. Gracias por detectarlo (por supuesto, si la implementación proviene de otra unidad de compilación, el compilador aún puede tener problemas para analizarlo, pero eso no fue lo que quise decir). Arreglaré la respuesta.
kriss
6

Hay varias vías para explicar esto, una de las cuales es el problema de detención :

En la teoría de la computabilidad, el problema de la detención se puede plantear de la siguiente manera: "Dada una descripción de un programa de computadora arbitrario, decida si el programa termina de ejecutarse o continúa ejecutándose para siempre". Esto es equivalente al problema de decidir, dado un programa y una entrada, si el programa finalmente se detendrá cuando se ejecute con esa entrada o si se ejecutará para siempre.

Alan Turing demostró en 1936 que no puede existir un algoritmo general para resolver el problema de detención para todos los posibles pares de entrada de programa.

Si escribo un programa que se parece a esto:

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

¿El valor del xcambio? Para determinar esto, primero tendría que determinar si la do tons of complex stuffpieza provoca que se active la condición o, lo que es más básico, si se detiene. Eso es algo que el compilador no puede hacer.

Timothy Shields
fuente
6

¡Realmente sorprendido de que no haya una respuesta que usar el problema de la detención directamente! Hay una reducción muy sencilla de este problema al problema de la detención.

Imagine que el compilador pudiera decir si una función cambió o no el valor de una variable. Entonces ciertamente podría decir si la siguiente función cambia el valor de y o no, asumiendo que el valor de x se puede rastrear en todas las llamadas durante el resto del programa:

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

Ahora, para cualquier programa que nos guste, vamos a reescribirlo como:

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

Observe que, si, y solo si, nuestro programa cambia el valor de y, entonces termina - foo () es lo último que hace antes de salir. ¡Esto significa que hemos resuelto el problema de la detención!

Lo que nos muestra la reducción anterior es que el problema de determinar si el valor de una variable cambia es al menos tan difícil como el problema de la detención. Se sabe que el problema de la detención es incomputable, por lo que este también debe serlo.

John Doucette
fuente
No estoy seguro de seguir su razonamiento sobre por qué nuestro programa termina si cambia el valor de y. Me parece que foo()regresa rápidamente y luego main()sale. (Además, llamas foo()sin discutir ... eso es parte de mi confusión.)
LarsH
1
@LarsH: Si el programa modificado termina, la última función que llamó fue f. Si se modificó y, se llamó a f (las otras declaraciones no pueden cambiar y, ya que solo fue introducido por la modificación). Por tanto, si se modificó y, el programa termina.
MSalters
4

Tan pronto como una función llama a otra función de la que el compilador no "ve" la fuente, debe asumir que la variable ha cambiado o las cosas pueden ir mal más adelante. Por ejemplo, digamos que tenemos esto en "foo.cpp":

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

y tenemos esto en "bar.cpp":

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

¿Cómo puede el compilador "saber" que xno está cambiando (o ESTÁ cambiando, más apropiadamente) bar?

Estoy seguro de que podemos llegar a algo más complejo, si esto no es lo suficientemente complejo.

Mats Petersson
fuente
El compilador puede saber que x no cambia en la barra si la barra x se pasa como paso por referencia a constante, ¿verdad?
Jugador de críquet
Sí, pero si agrego un const_castin foo, aún haría xcambios; estaría incumpliendo el contrato que dice que no debes cambiar las constvariables, pero como puedes convertir cualquier cosa a "más const", y const_castexiste, los diseñadores del lenguaje seguramente tenían en mente la idea de que a veces hay buenas razones para creer que los constvalores pueden necesitar un cambio.
Mats Petersson
@MatsPetersson: Creo que si const_cast puedes quedarte con todas las piezas que se rompen porque el compilador puede, pero no tiene que compensar eso.
Zan Lynx
@ZanLynx: Sí, estoy seguro de que es correcto. Pero al mismo tiempo, el elenco existe, lo que significa que alguien que diseñó el lenguaje tuvo algún tipo de idea de que "podríamos necesitar esto en algún momento", lo que significa que no está destinado a no hacer nada útil en absoluto.
Mats Petersson
1

Es imposible, en general, para el compilador para determinar si la variable se puede cambiar, ya que se han señalado.

Al verificar la constancia, la pregunta de interés parece ser si la variable puede ser cambiada por una función. Incluso esto es difícil en idiomas que admiten punteros. No puede controlar lo que hace otro código con un puntero, incluso podría leerse desde una fuente externa (aunque es poco probable). En lenguajes que restringen el acceso a la memoria, este tipo de garantías pueden ser posibles y permiten una optimización más agresiva que C ++.

Krumelur
fuente
2
Una cosa que desearía que se apoyara en los idiomas sería una distinción entre referencias (o punteros) efímeras, retornables y persistentes. Las referencias efímeras solo se pueden copiar a otras referencias efímeras, las retornables se pueden copiar a efímeras o retornables, y las persistentes se pueden copiar de cualquier forma. El valor de retorno de una función estará limitado por el más restrictivo de los argumentos que se pasan como parámetros "retornables". Considero lamentable que en muchos idiomas, cuando se pasa una referencia, no hay nada que indique cuánto tiempo se puede usar.
supercat
Eso sin duda sería útil. Por supuesto, existen patrones para esto, pero en C ++ (y muchos otros lenguajes) siempre es posible "hacer trampa".
Krumelur
Una de las principales formas en que .NET es superior a Java es que tiene un concepto de referencia efímera, pero desafortunadamente no hay forma de que los objetos expongan propiedades como referencias efímeras (lo que realmente me gustaría ver sería un medio por qué código usando una propiedad pasaría una referencia efímera a un código (junto con variables temporales) que debería usarse para manipular el objeto.
supercat
1

Para hacer la pregunta más específica, sugiero que el siguiente conjunto de restricciones puede haber sido lo que el autor del libro pudo haber tenido en mente:

  1. Suponga que el compilador está examinando el comportamiento de una función específica con respecto a la constancia de una variable. Para que sea correcto, un compilador tendría que asumir (debido al aliasing como se explica a continuación) si la función llama a otra función, la variable cambia, por lo que el supuesto n. ° 1 solo se aplica a fragmentos de código que no realizan llamadas a funciones.
  2. Suponga que la variable no es modificada por una actividad concurrente o asincrónica.
  3. Suponga que el compilador solo determina si la variable se puede modificar, no si se modificará. En otras palabras, el compilador solo realiza análisis estático.
  4. Suponga que el compilador solo está considerando el código que funciona correctamente (sin considerar los excesos / insuficiencias de la matriz, los punteros incorrectos, etc.)

En el contexto del diseño del compilador, creo que las suposiciones 1, 3, 4 tienen perfecto sentido en la vista de un escritor de compiladores en el contexto de la corrección de generación de código y / o la optimización de código. El supuesto 2 tiene sentido en ausencia de la palabra clave volátil. Y estas suposiciones también enfocan la pregunta lo suficiente como para hacer que juzgar una respuesta propuesta sea mucho más definitiva :-)

Dadas esas suposiciones, una razón clave por la que no se puede suponer la const-ness se debe al aliasing variable. El compilador no puede saber si otra variable apunta a la variable const. La creación de alias podría deberse a otra función en la misma unidad de compilación, en cuyo caso el compilador podría buscar entre funciones y usar un árbol de llamadas para determinar estáticamente que podría producirse la creación de alias. Pero si el alias se debe a una biblioteca u otro código externo, entonces el compilador no tiene forma de saber al ingresar la función si las variables tienen alias.

Se podría argumentar que si una variable / argumento está marcado como constante, no debería estar sujeto a cambios mediante alias, pero para un escritor de compiladores eso es bastante arriesgado. Incluso puede ser arriesgado para un programador humano declarar una variable const como parte de, digamos, un gran proyecto en el que no conoce el comportamiento de todo el sistema, o el sistema operativo, o una biblioteca, para saber realmente que una variable ganó ' t cambiar.

Χpẘ
fuente
0

Incluso si se declara una variable const, no significa que un código mal escrito pueda sobrescribirla.

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

salida:

1
2
Mark Lakata
fuente
Esto sucede porque ay bson variables de pila, y b[1]resulta que son la misma ubicación de memoria que a.
Mark Lakata
1
-1. Comportamiento indefinido elimina todas las restricciones sobre el comportamiento del compilador.
MSalters
No estoy seguro del voto negativo. Este es solo un ejemplo que va a la pregunta original del OP sobre por qué un compilador no puede averiguar si algo es realmente constsi todo está etiquetado const. Es porque el comportamiento indefinido es parte de C / C ++. Estaba tratando de encontrar una forma diferente de responder a su pregunta en lugar de mencionar el problema de la detención o la intervención humana externa.
Mark Lakata
0

Para ampliar mis comentarios, el texto de ese libro no está claro, lo que confunde el problema.

Como comenté, ese libro está tratando de decir: "Consigamos un número infinito de monos para escribir todas las funciones concebibles de C ++ que se puedan escribir. Habrá casos en los que si elegimos una variable que (alguna función particular que escribieron los monos) utiliza, no podemos determinar si la función cambiará esa variable ".

Por supuesto, para algunas (incluso muchas) funciones en cualquier aplicación, esto puede ser determinado por el compilador y muy fácilmente. Pero no para todos (o necesariamente para la mayoría).

Esta función se puede analizar fácilmente:

static int global;

void foo()
{
}

"foo" claramente no modifica "global". No modifica nada en absoluto, y un compilador puede resolver esto muy fácilmente.

Esta función no se puede analizar así:

static int global;

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

Dado que las acciones de "foo" dependen de un valor que puede cambiar en tiempo de ejecución , es evidente que no se puede determinar en tiempo de compilación. si modificará "global".

Todo este concepto es mucho más sencillo de entender de lo que los científicos informáticos creen. Si la función puede hacer algo diferente en función de las cosas que pueden cambiar en tiempo de ejecución, entonces no puede averiguar qué hará hasta que se ejecute, y cada vez que se ejecute puede hacer algo diferente. Ya sea demostrablemente imposible o no, obviamente es imposible.

El Zorko
fuente
Lo que dices es cierto, pero incluso para programas muy simples de los que se sabe todo en tiempo de compilación, no podrás probar nada, ni siquiera que el programa se detendrá. Este es el problema de la detención. Por ejemplo, podría escribir un programa basado en Hailstone Sequences en.wikipedia.org/wiki/Collatz_conjecture y hacer que devuelva verdadero si converge en uno. Los compiladores no podrán hacerlo (ya que se desbordaría en muchos casos) e incluso los matemáticos no saben si es cierto o no.
kriss
Si quiere decir "hay algunos programas de apariencia muy simple para los que no puede probar nada", estoy completamente de acuerdo. Pero la prueba clásica del problema de detención de Turing se basa esencialmente en que un programa por sí mismo sea capaz de decir si se detiene para establecer una contradicción. Como se trata de matemáticas, no de implementación. Ciertamente, hay programas en los que es completamente posible determinar estáticamente en tiempo de compilación si una variable en particular será modificada y si el programa se detendrá. Puede que no se pueda demostrar matemáticamente, pero es prácticamente alcanzable en ciertos casos.
El Zorko